From b9c7ef554b341d9139a20c383365edf2f490ad51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20Y=C4=B1ld=C4=B1r=C4=B1m?= Date: Mon, 15 Jun 2026 20:17:59 +0300 Subject: [PATCH 01/17] removed --- .eslintrc.json | 324 ---- .npmrc | 1 - .travis.yml | 5 - docma.json | 122 -- lib/notation.js | 3178 ------------------------------------ lib/notation.js.map | 1 - lib/notation.min.js | 2 - lib/notation.min.js.map | 1 - src/core/notation.error.js | 48 - src/core/notation.glob.js | 1118 ------------- src/core/notation.js | 1443 ---------------- src/index.js | 2 - src/utils.js | 214 --- test/ac.spec.js | 83 - test/filter.spec.js | 409 ----- test/notation.glob.spec.js | 963 ----------- test/notation.spec.js | 685 -------- test/utils.spec.js | 197 --- webpack.config.js | 100 -- 19 files changed, 8896 deletions(-) delete mode 100644 .eslintrc.json delete mode 100644 .npmrc delete mode 100644 .travis.yml delete mode 100644 docma.json delete mode 100644 lib/notation.js delete mode 100644 lib/notation.js.map delete mode 100644 lib/notation.min.js delete mode 100644 lib/notation.min.js.map delete mode 100644 src/core/notation.error.js delete mode 100644 src/core/notation.glob.js delete mode 100644 src/core/notation.js delete mode 100644 src/index.js delete mode 100644 src/utils.js delete mode 100644 test/ac.spec.js delete mode 100644 test/filter.spec.js delete mode 100644 test/notation.glob.spec.js delete mode 100644 test/notation.spec.js delete mode 100644 test/utils.spec.js delete mode 100644 webpack.config.js diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 329acf9..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,324 +0,0 @@ -{ - "parser": "babel-eslint", - "env": { - "es6": true, - "commonjs": true, - "node": true, - "browser": false, - "jest": true, - "jasmine": true - }, - "plugins": [], - "globals": { - "document": false, - "navigator": false, - "window": false - }, - "rules": { - - // ---------------------------- - // Possible Errors - // ---------------------------- - - "for-direction": 0, - "getter-return": 2, - "no-async-promise-executor": 2, - "no-await-in-loop": 2, - "no-compare-neg-zero": 2, - "no-cond-assign": 0, - "no-console": 1, - "no-constant-condition": 2, - "no-control-regex": 1, - "no-debugger": 2, - "no-dupe-args": 2, - "no-dupe-keys": 2, - "no-duplicate-case": 2, - "no-empty": 1, - "no-empty-character-class": 2, - "no-ex-assign": 2, - "no-extra-boolean-cast": 2, - "no-extra-parens": [0, { "conditionalAssign": false, "nestedBinaryExpressions": false }], - "no-extra-semi": 2, - "no-func-assign": 2, - "no-inner-declarations": 2, - "no-invalid-regexp": 2, - "no-irregular-whitespace": 1, - "no-misleading-character-class": 1, - "no-obj-calls": 2, - // https://eslint.org/docs/rules/no-prototype-builtins - "no-prototype-builtins": 2, - "no-regex-spaces": 2, - "no-sparse-arrays": 2, - "no-template-curly-in-string": 1, - "no-unexpected-multiline": 1, - "no-unreachable": 2, - "no-unsafe-finally": 2, - "no-unsafe-negation": 2, - "require-atomic-updates": 2, - "use-isnan": 2, - "valid-jsdoc": 1, - "valid-typeof": 2, - - // ---------------------------- - // Common / Best Practices - // ---------------------------- - - "accessor-pairs": 2, - "array-callback-return": 2, - "block-scoped-var": 2, - "class-methods-use-this": 1, - "complexity": [2, { "max": 25 }], - "consistent-return": [1, { "treatUndefinedAsUnspecified": true }], - "curly": [2, "multi-line", "consistent"], - "default-case": 2, - "dot-location": [2, "property"], - "dot-notation": 1, - "eqeqeq": 2, - "guard-for-in": 1, - "max-classes-per-file": [1, 1], - "no-alert": 2, - "no-caller": 2, - "no-case-declarations": 2, - "no-div-regex": 2, - "no-else-return": 2, - "no-empty-function": 1, - "no-empty-pattern": 2, - "no-eq-null": 2, - "no-eval": 2, - "no-extend-native": 2, - "no-extra-bind": 2, - "no-extra-label": 2, - "no-fallthrough": 2, - "no-floating-decimal": 2, - "no-global-assign": 2, - "no-implicit-coercion": 2, - "no-implicit-globals": 2, - "no-implied-eval": 2, - "no-invalid-this": 2, - "no-iterator": 2, - "no-labels": 2, - "no-lone-blocks": 2, - "no-loop-func": 2, - "no-magic-numbers": 0, - "no-multi-spaces": [1, { "ignoreEOLComments": true }], - "no-multi-str": 2, - "no-new": 2, - "no-new-func": 2, - "no-new-wrappers": 2, - "no-octal": 1, - "no-octal-escape": 2, - "no-param-reassign": 0, - "no-proto": 2, - "no-redeclare": 2, - // "no-restricted-properties": [], - "no-return-assign": 2, - "no-return-await": 2, - "no-script-url": 2, - "no-self-assign": 2, - "no-self-compare": 2, - "no-sequences": 2, - "no-throw-literal": 2, - "no-unmodified-loop-condition": 2, - "no-unused-expressions": 2, - "no-unused-labels": 2, - "no-useless-call": 2, - "no-useless-concat": 2, - "no-useless-escape": 1, - "no-useless-return": 2, - "no-void": 2, - "no-warning-comments": 0, - "no-with": 2, - "prefer-promise-reject-errors": 2, - "radix": 2, - "require-await": 2, - "require-unicode-regexp": 0, - "vars-on-top": 0, - "wrap-iife": [1, "inside"], - "yoda": [1, "never", { "exceptRange": true }], - - // ---------------------------- - // Strict Mode - // ---------------------------- - - "strict": [2, "safe"], - - // ---------------------------- - // Variables - // ---------------------------- - - "init-declarations": [0, "always"], - "no-delete-var": 2, - "no-label-var": 2, - // "no-restricted-globals": [1, "event", "fdescribe"], - "no-shadow": 2, - "no-shadow-restricted-names": 2, - "no-undef": 2, - "no-undef-init": 1, - "no-undefined": 0, - "no-unused-vars": [1, { "vars": "all", "varsIgnorePattern": "[iI]gnored?" }], - "no-use-before-define": 1, - - // ---------------------------- - // Node.js and CommonJS - // ---------------------------- - - "callback-return": 0, - "global-require": 1, - "handle-callback-err": 2, - "no-buffer-constructor": 2, - "no-mixed-requires": 1, - "no-new-require": 2, - "no-path-concat": 2, - "no-process-env": 2, - "no-process-exit": 1, - // "no-restricted-modules": ["error", [{ - // "name": "foo-module", - // "message": "Please use bar-module instead." - // }] - // ], - "no-sync": 1, - - // ---------------------------- - // Style Consistency - // ---------------------------- - - "array-bracket-newline": [2, "consistent"], - "array-element-newline": [2, "consistent"], - "block-spacing": [2, "always"], - "brace-style": [2, "1tbs", { "allowSingleLine": true }], - "camelcase": [2, { "properties": "always" }], - "capitalized-comments": 0, - "comma-dangle": [2, "never"], - "comma-spacing": [2, { "before": false, "after": true }], - "comma-style": [2, "last"], - "computed-property-spacing": [2, "never"], - "consistent-this": [2, "self"], - "eol-last": [2, "always"], - "func-call-spacing": [2, "never"], - "func-name-matching": [2, "always"], - "func-names": [1, "never"], - "func-style": 0, - "function-paren-newline": [1, "consistent"], - "id-blacklist": [2, "Class", "_this"], - "id-length": 0, - // "id-match": [2, "^[a-z]+([A-Z][a-z]+)*$"], - "implicit-arrow-linebreak": [2, "beside"], - "indent": [2, 4, { "SwitchCase": 1, "MemberExpression": 1, "FunctionDeclaration": { "body": 1, "parameters": 2 } }], - "jsx-quotes": [2, "prefer-double"], - "key-spacing": [2, { "beforeColon": false, "afterColon": true }], - "keyword-spacing": [2, { "before": true, "after": true }], - "line-comment-position": [0, { "position": "above" }], - "linebreak-style": [2, "unix"], - "lines-around-comment": 0, - "lines-between-class-members": [1, "always"], - "max-depth": [2, { "max": 4 }], - "max-len": [1, { "code": 120, "ignoreUrls": true, "ignoreRegExpLiterals": true, "ignoreTrailingComments": true, "ignoreStrings": true, "ignoreTemplateLiterals": true }], - "max-lines": [1, { "max": 500, "skipBlankLines": true, "skipComments": true }], - "max-lines-per-function": [1, { "max": 140, "skipBlankLines": true, "skipComments": true }], - "max-nested-callbacks": [1, { "max": 4 }], - "max-params": [1, { "max": 4 }], - "max-statements": [1, { "max": 30 }], - "max-statements-per-line": [2, { "max": 1 }], - "multiline-comment-style": [1, "separate-lines"], - "multiline-ternary": [1, "always-multiline"], - "new-cap": [1 , { "newIsCap": true, "capIsNew": true, "properties": true }], - "new-parens": 2, - "newline-per-chained-call": [1, { "ignoreChainWithDepth": 4 }], - "no-array-constructor": 2, - "no-bitwise": 1, - "no-continue": 0, - "no-inline-comments": 0, - "no-lonely-if": 0, - "no-mixed-operators": [1, { "allowSamePrecedence": true }], - "no-mixed-spaces-and-tabs": 2, - "no-multi-assign": 1, - "no-multiple-empty-lines": [2, { "max": 2 }], - "no-negated-condition": 0, - "no-nested-ternary": 0, - "no-new-object": 2, - "no-plusplus": 0, - "no-tabs": 2, - "no-ternary": 0, - "no-trailing-spaces": 1, - "no-underscore-dangle": 0, - "no-unneeded-ternary": 1, - "no-whitespace-before-property": 2, - "nonblock-statement-body-position": [2, "beside"], - "object-curly-newline": 0, - "object-curly-spacing": [2, "always"], - "object-property-newline": [2, { "allowAllPropertiesOnSameLine": true }], - "one-var": [2, { "initialized": "never" }], // "uninitialized": "always" - "one-var-declaration-per-line": [2, "initializations"], - "operator-assignment": [1, "always"], - "operator-linebreak": [1, "before"], - "padded-blocks": [1, { "classes": "always", "switches": "never" }], - "padding-line-between-statements": 0, - "prefer-object-spread": 1, - "quote-props": [2, "consistent-as-needed"], - "quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }], - "require-jsdoc": [0, { - "require": { - "FunctionDeclaration": false, - "MethodDefinition": true, - "ClassDeclaration": true, - "ArrowFunctionExpression": false, - "FunctionExpression": false - } - }], - "semi": [2, "always"], - "semi-spacing": [2, { "before": false, "after": true }], - "semi-style": [2, "last"], - "sort-keys": 0, - "sort-vars": 0, - "space-before-blocks": [2, "always"], - "space-before-function-paren": [2, { - "anonymous": "always", - "named": "never", - "asyncArrow": "always" - }], - "space-in-parens": [2, "never"], - "space-infix-ops": [2, { "int32Hint": false }], - "space-unary-ops": [2, { "words": true, "nonwords": false }], - "spaced-comment": [1, "always", { "exceptions": ["-", "*", "•", "+", "-+"] }], - "switch-colon-spacing": [2, { "after": true, "before": false }], - "template-tag-spacing": [1, "always"], - "unicode-bom": [2, "never"], - "wrap-regex": 1, - - // ---------------------------- - // Style Consistency - // ---------------------------- - - "arrow-body-style": [0, "as-needed"], // we need this but doesn't work well - "arrow-parens": [2, "as-needed"], - "arrow-spacing": [2, { "before": true, "after": true }], - "constructor-super": 2, - "generator-star-spacing": [2, { "before": true, "after": true }], - "no-class-assign": 2, - "no-confusing-arrow": 0, - "no-const-assign": 2, - "no-dupe-class-members": 2, - "no-duplicate-imports": [2, { "includeExports": true }], - "no-new-symbol": 2, - // "no-restricted-imports": ["error", { "paths": ["import1", "import2"] }], - "no-this-before-super": 2, - "no-useless-computed-key": 1, - "no-useless-constructor": 1, - "no-useless-rename": 2, - "no-var": 2, - "object-shorthand": [2, "always", { "avoidQuotes": true, "avoidExplicitReturnArrows": true }], - "prefer-arrow-callback": [2, { "allowUnboundThis": false }], - "prefer-const": 2, - "prefer-destructuring": [1, { "object": true, "array": false }, { "enforceForRenamedProperties": false }], - "prefer-numeric-literals": 0, - "prefer-rest-params": 2, - "prefer-spread": 2, - "prefer-template": 0, - "require-yield": 2, - "rest-spread-spacing": [2, "never"], - "sort-imports": [1, { "ignoreCase": true, "ignoreMemberSort": false }], - "symbol-description": 2, - "template-curly-spacing": [2, "never"], - "yield-star-spacing": [2, { "before": true, "after": true }] - } -} diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 9cf9495..0000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 93207bc..0000000 --- a/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -language: node_js -node_js: - - '12' - - '10' -before_script: cd $TRAVIS_BUILD_DIR diff --git a/docma.json b/docma.json deleted file mode 100644 index 2630ad2..0000000 --- a/docma.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "debug": 5, - "jsdoc": { - "encoding": "utf8", - "recurse": false, - "pedantic": false, - "access": ["public"], - "package": null, - "module": true, - "undocumented": false, - "undescribed": false, - "ignored": false, - "hierarchy": true, - "sort": "alphabetic", - "relativePath": null, - "filter": null, - "allowUnknownTags": true, - "plugins": [], - "includePattern": ".+\\.js(doc|x)?$" - }, - "markdown": { - "gfm": true, - "tables": true, - "breaks": false, - "pedantic": false, - "sanitize": false, - "smartLists": true, - "smartypants": false, - "tasks": true, - "emoji": true - }, - "app": { - "title": "Notation", - "meta": null, - "base": "/notation", - "entrance": "content:guide", - "routing": "path", - "server": "github" - }, - "template": { - "path": "zebra", - "options": { - "title": { - "label": "Notation", - "href": "/notation/" - }, - "logo": null, // URL String or { dark: String, light: String } - "sidebar": { - "enabled": true, - "outline": "tree", // "flat" | "tree" - "collapsed": false, - "toolbar": true, - "itemsFolded": false, - "itemsOverflow": "crop", // "crop" | "shrink" - "badges": true, // true | false | - "search": true, - "animations": true - }, - "symbols": { - "autoLink": true, // "internal" | "external" | true (both) - "params": "list", // "list" | "table" - "enums": "list", // "list" | "table" - "props": "list", // "list" | "table" - "meta": false - }, - "contentView": { - "bookmarks": "h1,h2,h3" - }, - "navbar": { - "enabled": true, - "fixed": true, - "dark": false, - "animations": true, - "menu": [ - { - "label": "Guide", - "href": "guide/" - }, - { - "label": "API Reference", - "href": "api/" - }, - { - "label": "Releases", - "items": [ - // { - // "label": "Link via CDNJS", - // "href": "https://cdnjs.com/libraries/notation", - // "target": "_blank" - // }, - // { "separator": true }, - { "label": "npm i notation" }, - { "separator": true }, - { - "label": "Download as Archive", - "href": "https://github.com/onury/notation/releases", - "target": "_blank" - }, - { "separator": true }, - { "label": "Change-Log", "href": "changelog/" } - ] - }, - { - "iconClass": "fa-lg fab fa-github", - "label": "GitHub", - "href": "https://github.com/onury/notation", - "target": "_blank" - } - ] - } - } - }, - "src": [ - "./src/core/**/*.js", - "./CHANGELOG.md", - { - "guide": "./README.md" - } - ], - "dest": "../onury.github.io/notation", - "clean": true -} diff --git a/lib/notation.js b/lib/notation.js deleted file mode 100644 index 10d019a..0000000 --- a/lib/notation.js +++ /dev/null @@ -1,3178 +0,0 @@ -(function webpackUniversalModuleDefinition(root, factory) { - if(typeof exports === 'object' && typeof module === 'object') - module.exports = factory(); - else if(typeof define === 'function' && define.amd) - define("notation", [], factory); - else if(typeof exports === 'object') - exports["notation"] = factory(); - else - root["notation"] = factory(); -})(this, function() { -return /******/ (function(modules) { // webpackBootstrap -/******/ // The module cache -/******/ var installedModules = {}; -/******/ -/******/ // The require function -/******/ function __webpack_require__(moduleId) { -/******/ -/******/ // Check if module is in cache -/******/ if(installedModules[moduleId]) { -/******/ return installedModules[moduleId].exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = installedModules[moduleId] = { -/******/ i: moduleId, -/******/ l: false, -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); -/******/ -/******/ // Flag the module as loaded -/******/ module.l = true; -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/******/ -/******/ // expose the modules object (__webpack_modules__) -/******/ __webpack_require__.m = modules; -/******/ -/******/ // expose the module cache -/******/ __webpack_require__.c = installedModules; -/******/ -/******/ // define getter function for harmony exports -/******/ __webpack_require__.d = function(exports, name, getter) { -/******/ if(!__webpack_require__.o(exports, name)) { -/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); -/******/ } -/******/ }; -/******/ -/******/ // define __esModule on exports -/******/ __webpack_require__.r = function(exports) { -/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { -/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); -/******/ } -/******/ Object.defineProperty(exports, '__esModule', { value: true }); -/******/ }; -/******/ -/******/ // create a fake namespace object -/******/ // mode & 1: value is a module id, require it -/******/ // mode & 2: merge all properties of value into the ns -/******/ // mode & 4: return value when already ns object -/******/ // mode & 8|1: behave like require -/******/ __webpack_require__.t = function(value, mode) { -/******/ if(mode & 1) value = __webpack_require__(value); -/******/ if(mode & 8) return value; -/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; -/******/ var ns = Object.create(null); -/******/ __webpack_require__.r(ns); -/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); -/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); -/******/ return ns; -/******/ }; -/******/ -/******/ // getDefaultExport function for compatibility with non-harmony modules -/******/ __webpack_require__.n = function(module) { -/******/ var getter = module && module.__esModule ? -/******/ function getDefault() { return module['default']; } : -/******/ function getModuleExports() { return module; }; -/******/ __webpack_require__.d(getter, 'a', getter); -/******/ return getter; -/******/ }; -/******/ -/******/ // Object.prototype.hasOwnProperty.call -/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; -/******/ -/******/ // __webpack_public_path__ -/******/ __webpack_require__.p = "lib/"; -/******/ -/******/ -/******/ // Load entry module and return exports -/******/ return __webpack_require__(__webpack_require__.s = "./src/index.js"); -/******/ }) -/************************************************************************/ -/******/ ({ - -/***/ "./src/core/notation.error.js": -/*!************************************!*\ - !*** ./src/core/notation.error.js ***! - \************************************/ -/*! exports provided: NotationError */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "NotationError", function() { return NotationError; }); -function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } - -function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } - -function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } - -function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } - -function _wrapNativeSuper(Class) { var _cache = typeof Map === "function" ? new Map() : undefined; _wrapNativeSuper = function _wrapNativeSuper(Class) { if (Class === null || !_isNativeFunction(Class)) return Class; if (typeof Class !== "function") { throw new TypeError("Super expression must either be null or a function"); } if (typeof _cache !== "undefined") { if (_cache.has(Class)) return _cache.get(Class); _cache.set(Class, Wrapper); } function Wrapper() { return _construct(Class, arguments, _getPrototypeOf(this).constructor); } Wrapper.prototype = Object.create(Class.prototype, { constructor: { value: Wrapper, enumerable: false, writable: true, configurable: true } }); return _setPrototypeOf(Wrapper, Class); }; return _wrapNativeSuper(Class); } - -function _construct(Parent, args, Class) { if (_isNativeReflectConstruct()) { _construct = Reflect.construct; } else { _construct = function _construct(Parent, args, Class) { var a = [null]; a.push.apply(a, args); var Constructor = Function.bind.apply(Parent, a); var instance = new Constructor(); if (Class) _setPrototypeOf(instance, Class.prototype); return instance; }; } return _construct.apply(null, arguments); } - -function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } } - -function _isNativeFunction(fn) { return Function.toString.call(fn).indexOf("[native code]") !== -1; } - -function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } - -function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } - -/* eslint consistent-this:0, no-prototype-builtins:0 */ -var setProto = Object.setPrototypeOf; -/** - * Error class specific to `Notation`. - * @class - * @name Notation.Error - */ - -var NotationError = /*#__PURE__*/function (_Error) { - _inherits(NotationError, _Error); - - var _super = _createSuper(NotationError); - - /** - * Initializes a new `Notation.Error` instance. - * @hideconstructor - * @constructs Notation.Error - * @param {String} message - The error message. - */ - function NotationError() { - var _this; - - var message = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; - - _classCallCheck(this, NotationError); - - _this = _super.call(this, message); - setProto(_assertThisInitialized(_this), NotationError.prototype); - Object.defineProperty(_assertThisInitialized(_this), 'name', { - enumerable: false, - writable: false, - value: 'NotationError' - }); - Object.defineProperty(_assertThisInitialized(_this), 'message', { - enumerable: false, - writable: true, - value: message - }); - /* istanbul ignore else */ - - if (Error.hasOwnProperty('captureStackTrace')) { - // V8 - Error.captureStackTrace(_assertThisInitialized(_this), NotationError); - } else { - Object.defineProperty(_assertThisInitialized(_this), 'stack', { - enumerable: false, - writable: false, - value: new Error(message).stack - }); - } - - return _this; - } - - return NotationError; -}( /*#__PURE__*/_wrapNativeSuper(Error)); - - - -/***/ }), - -/***/ "./src/core/notation.glob.js": -/*!***********************************!*\ - !*** ./src/core/notation.glob.js ***! - \***********************************/ -/*! exports provided: Glob */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Glob", function() { return Glob; }); -/* harmony import */ var _notation__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./notation */ "./src/core/notation.js"); -/* harmony import */ var _notation_error__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./notation.error */ "./src/core/notation.error.js"); -/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../utils */ "./src/utils.js"); -function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } - -function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } - -function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } - -/* eslint no-use-before-define:0, consistent-return:0, max-statements:0 */ - - - // http://www.linfo.org/wildcard.html -// http://en.wikipedia.org/wiki/Glob_%28programming%29 -// http://en.wikipedia.org/wiki/Wildcard_character#Computing -// created test @ https://regex101.com/r/U08luj/2 - -var reMATCHER = /(\[(\d+|\*|".*"|'.*')\]|[a-z$_][a-z$_\d]*|\*)/gi; // ! negation should be removed first -// created test @ https://regex101.com/r/mC8unE/3 -// /^!?(\*|[a-z$_][a-z$_\d]*|\[(\d+|".*"|'.*'|`.*`|\*)\])(\[(\d+|".*"|'.*'|`.*`|\*)\]|\.[a-z$_][a-z$_\d]*|\.\*)*$/i - -var reVALIDATOR = new RegExp('^' + '!?(' // optional negation, only in the front -+ '\\*' // wildcard star -+ '|' // OR -+ '[a-z$_][a-z$_\\d]*' // JS variable syntax -+ '|' // OR -+ '\\[(\\d+|\\*|".*"|\'.*\')\\]' // array index or wildcard, or object bracket notation -+ ')' // exactly once -+ '(' + '\\[(\\d+|\\*|".*"|\'.*\')\\]' // followed by same -+ '|' // OR -+ '\\.[a-z$_][a-z$_\\d]*' // dot, then JS variable syntax -+ '|' // OR -+ '\\.\\*' // dot, then wildcard star -+ ')*' // (both) may repeat any number of times -+ '$', 'i'); -var re = _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].re; -var ERR_INVALID = 'Invalid glob notation: '; -/** - * `Notation.Glob` is a utility for validating, comparing and sorting - * dot-notation globs. - * - * You can use {@link http://www.linfo.org/wildcard.html|wildcard} stars `*` - * and negate the notation by prepending a bang `!`. A star will include all - * the properties at that level and a negated notation will be excluded. - * @name Glob - * @memberof Notation - * @class - * - * @example - * // for the following object; - * { name: 'John', billing: { account: { id: 1, active: true } } }; - * - * 'billing.account.*' // represents value `{ id: 1, active: true }` - * 'billing.account.id' // represents value `1` - * '!billing.account.*' // represents value `{ name: 'John' }` - * 'name' // represents `'John'` - * '*' // represents the whole object - * - * @example - * var glob = new Notation.Glob('billing.account.*'); - * glob.test('billing.account.id'); // true - */ - -var Glob = /*#__PURE__*/function () { - /** - * Constructs a `Notation.Glob` object with the given glob string. - * @constructs Notation.Glob - * @param {String} glob - Notation string with globs. - * - * @throws {NotationError} - If given notation glob is invalid. - */ - function Glob(glob) { - _classCallCheck(this, Glob); - - var ins = Glob._inspect(glob); - - var notes = Glob.split(ins.absGlob); - this._ = _objectSpread(_objectSpread({}, ins), {}, { - notes: notes, - // below props will be set at first getter call - parent: undefined, - // don't set to null - regexp: undefined - }); - } // -------------------------------- - // INSTANCE PROPERTIES - // -------------------------------- - - /** - * Gets the normalized glob notation string. - * @name Notation.Glob#glob - * @type {String} - */ - - - _createClass(Glob, [{ - key: "test", - // -------------------------------- - // INSTANCE METHODS - // -------------------------------- - - /** - * Checks whether the given notation value matches the source notation - * glob. - * @name Notation.Glob#test - * @function - * @param {String} notation - The notation string to be tested. Cannot have - * any globs. - * @returns {Boolean} - - * @throws {NotationError} - If given `notation` is not valid or contains - * any globs. - * - * @example - * const glob = new Notation.Glob('!prop.*.name'); - * glob.test("prop.account.name"); // true - */ - value: function test(notation) { - if (!_notation__WEBPACK_IMPORTED_MODULE_0__["Notation"].isValid(notation)) { - throw new _notation_error__WEBPACK_IMPORTED_MODULE_1__["NotationError"]("Invalid notation: '".concat(notation, "'")); - } // return this.regexp.test(notation); - - - return Glob._covers(this, notation); - } - /** - * Specifies whether this glob notation can represent (or cover) the given - * glob notation. Note that negation prefix is ignored, if any. - * @name Notation.Glob#covers - * @function - * - * @param {String|Array|Glob} glob Glob notation string, glob - * notes array or a `Notation.Glob` instance. - * @returns {Boolean} - - * - * @example - * const glob = Notation.Glob.create; - * glob('*.y').covers('x.y') // true - * glob('x[*].y').covers('x[*]') // false - */ - - }, { - key: "covers", - value: function covers(glob) { - return Glob._covers(this, glob); - } - /** - * Gets the intersection of this and the given glob notations. When - * restrictive, if any one of them is negated, the outcome is negated. - * Otherwise, only if both of them are negated, the outcome is negated. - * @name Notation.Glob#intersect - * @function - * - * @param {String} glob - Second glob to be used. - * @param {Boolean} [restrictive=false] - Whether the intersection should - * be negated when one of the globs is negated. - * @returns {String} - Intersection notation if any; otherwise `null`. - * - * @example - * const glob = Notation.Glob.create; - * glob('x.*').intersect('!*.y') // 'x.y' - * glob('x.*').intersect('!*.y', true) // '!x.y' - */ - - }, { - key: "intersect", - value: function intersect(glob) { - var restrictive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; - return Glob._intersect(this.glob, glob, restrictive); - } // -------------------------------- - // STATIC MEMBERS - // -------------------------------- - - /** - * Basically constructs a new `Notation.Glob` instance - * with the given glob string. - * @name Notation.Glob.create - * @function - * - * @param {String} glob - The source notation glob. - * @returns {Glob} - - * - * @example - * const glob = Notation.Glob.create(strGlob); - * // equivalent to: - * const glob = new Notation.Glob(strGlob); - */ - - }, { - key: "glob", - get: function get() { - return this._.glob; - } - /** - * Gets the absolute glob notation without the negation prefix `!` and - * redundant trailing wildcards. - * @name Notation.Glob#absGlob - * @type {String} - */ - - }, { - key: "absGlob", - get: function get() { - return this._.absGlob; - } - /** - * Specifies whether this glob is negated with a `!` prefix. - * @name Notation.Glob#isNegated - * @type {Boolean} - */ - - }, { - key: "isNegated", - get: function get() { - return this._.isNegated; - } - /** - * Represents this glob in regular expressions. - * Note that the negation prefix (`!`) is ignored, if any. - * @name Notation.Glob#regexp - * @type {RegExp} - */ - - }, { - key: "regexp", - get: function get() { - // setting on first call instead of in constructor, for performance - // optimization. - this._.regexp = this._.regexp || Glob.toRegExp(this.absGlob); - return this._.regexp; - } - /** - * List of notes (levels) of this glob notation. Note that trailing, - * redundant wildcards are removed from the original glob notation. - * @name Notation.Glob#notes - * @alias Notation.Glob#levels - * @type {Array} - */ - - }, { - key: "notes", - get: function get() { - return this._.notes; - } - /** - * Alias of `Notation.Glob#notes`. - * @private - * @name Notation.Glob#notes - * @alias Notation.Glob#levels - * @type {Array} - */ - - }, { - key: "levels", - get: function get() { - return this._.notes; - } - /** - * Gets the first note of this glob notation. - * @name Notation.Glob#first - * @type {String} - */ - - }, { - key: "first", - get: function get() { - return this.notes[0]; - } - /** - * Gets the last note of this glob notation. - * @name Notation.Glob#last - * @type {String} - */ - - }, { - key: "last", - get: function get() { - return this.notes[this.notes.length - 1]; - } - /** - * Gets the parent notation (up to but excluding the last note) from the - * glob notation string. Note that initially, trailing/redundant wildcards - * are removed. - * @name Notation.Glob#parent - * @type {String} - * - * @example - * const glob = Notation.Glob.create; - * glob('first.second.*').parent; // "first.second" - * glob('*.x.*').parent; // "*" ("*.x.*" normalizes to "*.x") - * glob('*').parent; // null (no parent) - */ - - }, { - key: "parent", - get: function get() { - // setting on first call instead of in constructor, for performance - // optimization. - if (this._.parent === undefined) { - this._.parent = this.notes.length > 1 ? this.absGlob.slice(0, -this.last.length).replace(/\.$/, '') : null; - } - - return this._.parent; - } - }], [{ - key: "create", - value: function create(glob) { - return new Glob(glob); - } // Created test at: https://regex101.com/r/tJ7yI9/4 - - /** - * Validates the given notation glob. - * @name Notation.Glob.isValid - * @function - * - * @param {String} glob - Notation glob to be validated. - * @returns {Boolean} - - */ - - }, { - key: "isValid", - value: function isValid(glob) { - return typeof glob === 'string' && reVALIDATOR.test(glob); - } - /** - * Specifies whether the given glob notation includes any valid wildcards - * (`*`) or negation bang prefix (`!`). - * @name Notation.Glob.hasMagic - * @function - * - * @param {String} glob - Glob notation to be checked. - * @returns {Boolean} - - */ - - }, { - key: "hasMagic", - value: function hasMagic(glob) { - return Glob.isValid(glob) && (re.WILDCARDS.test(glob) || glob[0] === '!'); - } - /** - * Gets a regular expressions instance from the given glob notation. - * Note that the bang `!` prefix will be ignored if the given glob is negated. - * @name Notation.Glob.toRegExp - * @function - * - * @param {String} glob - Glob notation to be converted. - * - * @returns {RegExp} - A `RegExp` instance from the glob. - * - * @throws {NotationError} - If given notation glob is invalid. - */ - - }, { - key: "toRegExp", - value: function toRegExp(glob) { - if (!Glob.isValid(glob)) { - throw new _notation_error__WEBPACK_IMPORTED_MODULE_1__["NotationError"]("".concat(ERR_INVALID, " '").concat(glob, "'")); - } - - var g = glob.indexOf('!') === 0 ? glob.slice(1) : glob; - g = _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].pregQuote(g) // `[*]` always represents array index e.g. `[1]`. so we'd replace - // `\[\*\]` with `\[\d+\]` but we should also watch for quotes: e.g. - // `["x[*]y"]` - .replace(/\\\[\\\*\\\](?=(?:[^"]|"[^"]*")*$)(?=(?:[^']|'[^']*')*$)/g, '\\[\\d+\\]') // `*` within quotes (e.g. ['*']) is non-wildcard, just a regular star char. - // `*` outside of quotes is always JS variable syntax e.g. `prop.*` - .replace(/\\\*(?=(?:[^"]|"[^"]*")*$)(?=(?:[^']|'[^']*')*$)/g, '[a-z$_][a-z$_\\d]*').replace(/\\\?/g, '.'); - return new RegExp('^' + g + '(?:[\\[\\.].+|$)', 'i'); // it should either end ($) or continue with a dot or bracket. So for - // example, `company.*` will produce `/^company\.[a-z$_][a-z$_\\d]*(?:[\\[|\\.].+|$)/` - // which will match both `company.name` and `company.address.street` but - // will not match `some.company.name`. Also `!password` will not match - // `!password_reset`. - } - /** - * Specifies whether first glob notation can represent (or cover) the - * second. - * @name Notation.Glob._covers - * @function - * @private - * - * @param {String|Object|Glob} globA Source glob notation string - * or inspection result object or `Notation.Glob` instance. - * @param {String|Object|Glob} globB Glob notation string or - * inspection result object or `Notation.Glob` instance. - * @param {Boolean} [match=false] Check whether notes match instead of - * `globA` covers `globB`. - * @returns {Boolean} - - * - * @example - * const { covers } = Notation.Glob; - * covers('*.y', 'x.y') // true - * covers('x.y', '*.y') // false - * covers('x.y', '*.y', true) // true - * covers('x[*].y', 'x[*]') // false - */ - - }, { - key: "_covers", - value: function _covers(globA, globB) { - var match = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; - var a = typeof globA === 'string' ? new Glob(globA) : globA; // assume (globA instanceof Notation.Glob || utils.type(globA) === 'object') - - var b = typeof globB === 'string' ? new Glob(globB) : globB; - var notesA = a.notes || Glob.split(a.absGlob); - var notesB = b.notes || Glob.split(b.absGlob); - - if (!match) { - // !x.*.* does not cover !x.* or x.* bec. !x.*.* ≠ x.* ≠ x - // x.*.* covers x.* bec. x.*.* = x.* = x - if (a.isNegated && notesA.length > notesB.length) return false; - } - - var covers = true; - var fn = match ? _matchesNote : _coversNote; - - for (var i = 0; i < notesA.length; i++) { - if (!fn(notesA[i], notesB[i])) { - covers = false; - break; - } - } - - return covers; - } - /** - * Gets the intersection notation of two glob notations. When restrictive, - * if any one of them is negated, the outcome is negated. Otherwise, only - * if both of them are negated, the outcome is negated. - * @name Notation.Glob._intersect - * @function - * @private - * - * @param {String} globA - First glob to be used. - * @param {String} globB - Second glob to be used. - * @param {Boolean} [restrictive=false] - Whether the intersection should - * be negated when one of the globs is negated. - * @returns {String} - Intersection notation if any; otherwise `null`. - * @example - * _intersect('!*.y', 'x.*', false) // 'x.y' - * _intersect('!*.y', 'x.*', true) // '!x.y' - */ - - }, { - key: "_intersect", - value: function _intersect(globA, globB) { - var restrictive = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; - // const bang = restrictive - // ? (globA[0] === '!' || globB[0] === '!' ? '!' : '') - // : (globA[0] === '!' && globB[0] === '!' ? '!' : ''); - var notesA = Glob.split(globA, true); - var notesB = Glob.split(globB, true); - var bang; - - if (restrictive) { - bang = globA[0] === '!' || globB[0] === '!' ? '!' : ''; - } else { - if (globA[0] === '!' && globB[0] === '!') { - bang = '!'; - } else { - bang = notesA.length > notesB.length && globA[0] === '!' || notesB.length > notesA.length && globB[0] === '!' ? '!' : ''; - } - } - - var len = Math.max(notesA.length, notesB.length); - var notesI = []; - var a, b; // x.* ∩ *.y » x.y - // x.*.* ∩ *.y » x.y.* - // x.*.z ∩ *.y » x.y.z - // x.y ∩ *.b » (n/a) - // x.y ∩ a.* » (n/a) - - for (var i = 0; i < len; i++) { - a = notesA[i]; - b = notesB[i]; - - if (a === b) { - notesI.push(a); - } else if (a && re.WILDCARD.test(a)) { - if (!b) { - notesI.push(a); - } else { - notesI.push(b); - } - } else if (b && re.WILDCARD.test(b)) { - if (!a) { - notesI.push(b); - } else { - notesI.push(a); - } - } else if (a && !b) { - notesI.push(a); - } else if (!a && b) { - notesI.push(b); - } else { - // if (a !== b) { - notesI = []; - break; - } - } - - if (notesI.length > 0) return bang + _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].joinNotes(notesI); - return null; - } - /** - * Undocumented. - * @name Notation.Glob._inspect - * @function - * @private - * - * @param {String} glob - - * @returns {Object} - - */ - - }, { - key: "_inspect", - value: function _inspect(glob) { - var g = glob.trim(); - - if (!Glob.isValid(g)) { - throw new _notation_error__WEBPACK_IMPORTED_MODULE_1__["NotationError"]("".concat(ERR_INVALID, " '").concat(glob, "'")); - } - - var isNegated = g[0] === '!'; // trailing wildcards are only redundant if not negated - - if (!isNegated) g = _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].removeTrailingWildcards(g); - var absGlob = isNegated ? g.slice(1) : g; - return { - glob: g, - absGlob: absGlob, - isNegated: isNegated, - // e.g. [*] or [1] are array globs. ["1"] is not. - isArrayGlob: /^\[[^'"]/.test(absGlob) - }; - } - /** - * Splits the given glob notation string into its notes (levels). Note that - * this will exclude the `!` negation prefix, if it exists. - * @name Notation.Glob.split - * @function - * - * @param {String} glob Glob notation string to be splitted. - * @param {String} [normalize=false] Whether to remove trailing, redundant - * wildcards. - * @returns {Array} - A string array of glob notes (levels). - * @throws {NotationError} - If given glob notation is invalid. - * - * @example - * Notation.Glob.split('*.list[2].prop') // ['*', 'list', '[2]', 'prop'] - * // you can get the same result from the .notes property of a Notation.Glob instance. - */ - - }, { - key: "split", - value: function split(glob) { - var normalize = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; - - if (!Glob.isValid(glob)) { - throw new _notation_error__WEBPACK_IMPORTED_MODULE_1__["NotationError"]("".concat(ERR_INVALID, " '").concat(glob, "'")); - } - - var neg = glob[0] === '!'; // trailing wildcards are redundant only when not negated - - var g = !neg && normalize ? _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].removeTrailingWildcards(glob) : glob; - return g.replace(/^!/, '').match(reMATCHER); - } - /** - * Compares two given notation globs and returns an integer value as a - * result. This is generally used to sort glob arrays. Loose globs (with - * stars especially closer to beginning of the glob string) and globs - * representing the parent/root of the compared property glob come first. - * Verbose/detailed/exact globs come last. (`* < *.abc < abc`). - * - * For instance; `store.address` comes before `store.address.street`. So - * this works both for `*, store.address.street, !store.address` and `*, - * store.address, !store.address.street`. For cases such as `prop.id` vs - * `!prop.id` which represent the same property; the negated glob comes - * last. - * @name Notation.Glob.compare - * @function - * - * @param {String} globA - First notation glob to be compared. - * @param {String} globB - Second notation glob to be compared. - * - * @returns {Number} - Returns `-1` if `globA` comes first, `1` if `globB` - * comes first and `0` if equivalent priority. - * - * @throws {NotationError} - If either `globA` or `globB` is invalid glob - * notation. - * - * @example - * const { compare } = Notation.Glob; - * compare('*', 'info.user') // -1 - * compare('*', '[*]') // 0 - * compare('info.*.name', 'info.user') // 1 - */ - - }, { - key: "compare", - value: function compare(globA, globB) { - // trivial case, both are exactly the same! - // or both are wildcard e.g. `*` or `[*]` - if (globA === globB || re.WILDCARD.test(globA) && re.WILDCARD.test(globB)) return 0; - var a = new Glob(globA); - var b = new Glob(globB); // Check depth (number of levels) - - if (a.notes.length === b.notes.length) { - // check and compare if these are globs that represent items in the - // "same" array. if not, this will return 0. - var aIdxCompare = _compareArrayItemGlobs(a, b); // we'll only continue comparing if 0 is returned - - - if (aIdxCompare !== 0) return aIdxCompare; // count wildcards - - var wildCountA = (a.absGlob.match(re.WILDCARDS) || []).length; - var wildCountB = (b.absGlob.match(re.WILDCARDS) || []).length; - - if (wildCountA === wildCountB) { - // check for negation - if (!a.isNegated && b.isNegated) return -1; - if (a.isNegated && !b.isNegated) return 1; // both are negated or neither are, return alphabetical - - return a.absGlob < b.absGlob ? -1 : a.absGlob > b.absGlob ? 1 : 0; - } - - return wildCountA > wildCountB ? -1 : 1; - } - - return a.notes.length < b.notes.length ? -1 : 1; - } - /** - * Sorts the notation globs in the given array by their priorities. Loose - * globs (with stars especially closer to beginning of the glob string); - * globs representing the parent/root of the compared property glob come - * first. Verbose/detailed/exact globs come last. (`* < *.y < x.y`). - * - * For instance; `store.address` comes before `store.address.street`. For - * cases such as `prop.id` vs `!prop.id` which represent the same property; - * the negated glob wins (comes last). - * @name Notation.Glob.sort - * @function - * - * @param {Array} globList - The notation globs array to be sorted. The - * passed array reference is modified. - * @returns {Array} - Logically sorted globs array. - * - * @example - * Notation.Glob.sort(['!prop.*.name', 'prop.*', 'prop.id']) // ['prop.*', 'prop.id', '!prop.*.name']; - */ - - }, { - key: "sort", - value: function sort(globList) { - return globList.sort(Glob.compare); - } - /** - * Normalizes the given notation globs array by removing duplicate or - * redundant items, eliminating extra verbosity (also with intersection - * globs) and returns a priority-sorted globs array. - * - * - * @name Notation.Glob.normalize - * @function - * - * @param {Array} globList - Notation globs array to be normalized. - * @param {Boolean} [restrictive=false] - Whether negated items strictly - * remove every match. Note that, regardless of this option, if any item has an - * exact negated version; non-negated is always removed. - * @returns {Array} - - * - * @throws {NotationError} - If any item in globs list is invalid. - * - * @example - * const { normalize } = Notation.Glob; - * normalize(['*', '!id', 'name', '!car.model', 'car.*', 'id', 'name']); // ['*', '!id', '!car.model'] - * normalize(['!*.id', 'user.*', 'company']); // ['company', 'user', '!company.id', '!user.id'] - * normalize(['*', 'car.model', '!car.*']); // ["*", "!car.*", "car.model"] - * // restrictive normalize: - * normalize(['*', 'car.model', '!car.*'], true); // ["*", "!car.*"] - */ - - }, { - key: "normalize", - value: function normalize(globList) { - var restrictive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; - var _inspect = Glob._inspect, - _covers = Glob._covers, - _intersect = Glob._intersect; - var original = _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].ensureArray(globList); - if (original.length === 0) return []; - var list = original // prevent mutation - .concat() // move negated globs to top so that we inspect non-negated globs - // against others first. when complete, we'll sort with our - // .compare() function. - .sort(restrictive ? _negFirstSort : _negLastSort) // turning string array into inspect-obj array, so that we'll not - // run _inspect multiple times in the inner loop. this also - // pre-validates each glob. - .map(_inspect); // early return if we have a single item - - if (list.length === 1) { - var g = list[0]; // single negated item is redundant - - if (g.isNegated) return []; // return normalized - - return [g.glob]; - } // flag to return an empty array (in restrictive mode), if true. - - - var negateAll = false; // we'll push keepers in this array - - var normalized = []; // we'll need to remember excluded globs, so that we can move to next - // item early. - - var ignored = {}; // storage to keep intersections. - // using an object to prevent duplicates. - - var intersections = {}; - - function checkAddIntersection(gA, gB) { - var inter = _intersect(gA, gB, restrictive); - - if (!inter) return; // if the intersection result has an inverted version in the - // original list, don't add this. - - var hasInverted = restrictive ? false : original.indexOf(_invert(inter)) >= 0; // also if intersection result is in the current list, don't add it. - - if (list.indexOf(inter) >= 0 || hasInverted) return; - intersections[inter] = inter; - } // iterate each glob by comparing it to remaining globs. - - - _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].eachRight(list, function (a, indexA) { - // if `strict` is enabled, return empty if a negate-all is found - // (which itself is also redundant if single): '!*' or '![*]' - if (re.NEGATE_ALL.test(a.glob)) { - negateAll = true; - if (restrictive) return false; - } // flags - - - var duplicate = false; - var hasExactNeg = false; // flags for negated - - var negCoversPos = false; - var negCoveredByPos = false; - var negCoveredByNeg = false; // flags for non-negated (positive) - - var posCoversPos = false; - var posCoveredByNeg = false; - var posCoveredByPos = false; - _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].eachRight(list, function (b, indexB) { - // don't inspect glob with itself - if (indexA === indexB) return; // move to next - // console.log(indexA, a.glob, 'vs', b.glob); - - if (a.isArrayGlob !== b.isArrayGlob) { - throw new _notation_error__WEBPACK_IMPORTED_MODULE_1__["NotationError"]("Integrity failed. Cannot have both object and array notations for root level: ".concat(JSON.stringify(original))); - } // remove if duplicate - - - if (a.glob === b.glob) { - list.splice(indexA, 1); - duplicate = true; - return false; // break out - } // remove if positive has an exact negated (negated wins when - // normalized) e.g. ['*', 'a', '!a'] => ['*', '!a'] - - - if (!a.isNegated && _isReverseOf(a, b)) { - // list.splice(indexA, 1); - ignored[a.glob] = true; - hasExactNeg = true; - return false; // break out - } // if already excluded b, go on to next - - - if (ignored[b.glob]) return; // next - - var coversB = _covers(a, b); - - var coveredByB = coversB ? false : _covers(b, a); - - if (a.isNegated) { - if (b.isNegated) { - // if negated (a) covered by any other negated (b); remove (a)! - if (coveredByB) { - negCoveredByNeg = true; // list.splice(indexA, 1); - - ignored[a.glob] = true; - return false; // break out - } - } else { - /* istanbul ignore if */ - if (coversB) negCoversPos = true; - if (coveredByB) negCoveredByPos = true; // try intersection if none covers the other and only - // one of them is negated. - - if (!coversB && !coveredByB) { - checkAddIntersection(a.glob, b.glob); - } - } - } else { - if (b.isNegated) { - // if positive (a) covered by any negated (b); remove (a)! - if (coveredByB) { - posCoveredByNeg = true; - - if (restrictive) { - // list.splice(indexA, 1); - ignored[a.glob] = true; - return false; // break out - } - - return; // next - } // try intersection if none covers the other and only - // one of them is negated. - - - if (!coversB && !coveredByB) { - checkAddIntersection(a.glob, b.glob); - } - } else { - if (coversB) posCoversPos = coversB; // if positive (a) covered by any other positive (b); remove (a)! - - if (coveredByB) { - posCoveredByPos = true; - - if (restrictive) { - // list.splice(indexA, 1); - return false; // break out - } - } - } - } - }); // const keepNeg = (negCoversPos || negCoveredByPos) && !negCoveredByNeg; - - var keepNeg = restrictive ? (negCoversPos || negCoveredByPos) && negCoveredByNeg === false : negCoveredByPos && negCoveredByNeg === false; - var keepPos = restrictive ? (posCoversPos || posCoveredByPos === false) && posCoveredByNeg === false : posCoveredByNeg || posCoveredByPos === false; - var keep = duplicate === false && hasExactNeg === false && (a.isNegated ? keepNeg : keepPos); - - if (keep) { - normalized.push(a.glob); - } else { - // this is excluded from final (normalized) list, so mark as - // ignored (don't remove from "list" for now) - ignored[a.glob] = true; - } - }); - if (restrictive && negateAll) return []; - intersections = Object.keys(intersections); - - if (intersections.length > 0) { - // merge normalized list with intersections if any - normalized = normalized.concat(intersections); // we have new (intersection) items, so re-normalize - - return Glob.normalize(normalized, restrictive); - } - - return Glob.sort(normalized); - } - /** - * Undocumented. See `.union()` - * @name Notation.Glob._compareUnion - * @function - * @private - * - * @param {Array} globsListA - - * @param {Array} globsListB - - * @param {Boolean} restrictive - - * @param {Array} union - - * @returns {Array} - - */ - - }, { - key: "_compareUnion", - value: function _compareUnion(globsListA, globsListB, restrictive) { - var union = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : []; - var _covers = Glob._covers; - var _inspect = Glob._inspect, - _intersect = Glob._intersect; - _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].eachRight(globsListA, function (globA) { - if (union.indexOf(globA) >= 0) return; // next - - var a = _inspect(globA); // if wildcard only, add... - - - if (re.WILDCARD.test(a.absGlob)) { - union.push(a.glob); // push normalized glob - - return; // next - } - - var notCovered = false; - var hasExact = false; - var negCoversNeg = false; - var posCoversNeg = false; - var posCoversPos = false; - var negCoversPos = false; - var intersections = []; - _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].eachRight(globsListB, function (globB) { - // keep if has exact in the other - if (globA === globB) hasExact = true; - - var b = _inspect(globB); // keep negated if: - // 1) any negated covers it - // 2) no positive covers it - // keep positive if: - // 1) no positive covers it OR any negated covers it - - - notCovered = !_covers(b, a); - - if (notCovered) { - if (a.isNegated && b.isNegated) { - var inter = _intersect(a.glob, b.glob, restrictive); - - if (inter && union.indexOf(inter) === -1) intersections.push(inter); - } - - return; // next - } - - if (a.isNegated) { - if (b.isNegated) { - negCoversNeg = !hasExact; - } else { - posCoversNeg = true; // set flag - } - } else { - if (!b.isNegated) { - posCoversPos = !hasExact; - } else { - negCoversPos = true; // set flag - } - } - }); - var keep = a.isNegated ? !posCoversNeg || negCoversNeg : !posCoversPos || negCoversPos; - - if (hasExact || keep || notCovered && !a.isNegated) { - union.push(a.glob); // push normalized glob - - return; - } - - if (a.isNegated && posCoversNeg && !negCoversNeg && intersections.length > 0) { - union = union.concat(intersections); - } - }); - return union; - } - /** - * Gets the union from the given couple of glob arrays and returns a new - * array of globs. - * - * @name Notation.Glob.union - * @function - * - * @param {Array} globsA - First array of glob strings. - * @param {Array} globsB - Second array of glob strings. - * @param {Boolean} [restrictive=false] - Whether negated items in each of - * the lists, strictly remove every match in themselves (not the cross - * list). This option is used when pre-normalizing each glob list and - * normalizing the final union list. - * - * @returns {Array} - - * - * @example - * const a = ['user.*', '!user.email', 'car.model', '!*.id']; - * const b = ['!*.date', 'user.email', 'car', '*.age']; - * const { union } = Notation.Glob; - * union(a, b) // ['car', 'user', '*.age', '!car.date', '!user.id'] - */ - - }, { - key: "union", - value: function union(globsA, globsB, restrictive) { - var normalize = Glob.normalize, - _compareUnion = Glob._compareUnion; - var listA = normalize(globsA, restrictive); - var listB = normalize(globsB, restrictive); - if (listA.length === 0) return listB; - if (listB.length === 0) return listA; // TODO: below should be optimized - - var union = _compareUnion(listA, listB, restrictive); - - union = _compareUnion(listB, listA, restrictive, union); - return normalize(union, restrictive); - } - }]); - - return Glob; -}(); // -------------------------------- -// HELPERS -// -------------------------------- -// used by static _covers - - -function _coversNote(a, b) { - if (!a || !b) return false; // glob e.g.: [2] does not cover [2][1] - - var bIsArr = re.ARRAY_GLOB_NOTE.test(b); // obj-wildcard a will cover b if not array - - if (a === '*') return !bIsArr; // arr-wildcard a will cover b if array - - if (a === '[*]') return bIsArr; // seems, a is not wildcard so, - // if b is wildcard (obj or arr) won't be covered - - if (re.WILDCARD.test(b)) return false; // normalize both and check for equality - // e.g. x.y and x['y'] are the same - - return _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].normalizeNote(a) === _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].normalizeNote(b); -} // function _coversNote(a, b) { -// if (!a || !b) return false; // glob e.g.: [2] does not cover [2][1] -// a = utils.normalizeNote(a, true); -// b = utils.normalizeNote(b, true); -// if (a === b) return true; -// const bIsArr = re.ARRAY_GLOB_NOTE.test(b); -// return (a === '*' && !bIsArr) || (a === '[*]' && bIsArr); -// } -// used by static _covers - - -function _matchesNote(a, b) { - if (!a || !b) return true; // glob e.g.: [2][1] matches [2] and vice-versa. - - return _coversNote(a, b) || _coversNote(b, a); -} // used by _compareArrayItemGlobs() for getting a numeric index from array note. -// we'll use these indexes to sort higher to lower, as removing order; to -// prevent shifted indexes. - - -function _idxVal(note) { - // we return -1 for wildcard bec. we need it to come last - // below will never execute when called from _compareArrayItemGlobs - - /* istanbul ignore next */ - // if (note === '[*]') return -1; - // e.g. '[2]' » 2 - return parseInt(note.replace(/[[\]]/, ''), 10); -} - -function _compArrIdx(lastA, lastB) { - var iA = _idxVal(lastA); - - var iB = _idxVal(lastB); // below will never execute when called from _compareArrayItemGlobs - - /* istanbul ignore next */ - // if (iA === iB) return 0; - - - return iA > iB ? -1 : 1; -} // when we remove items from an array (via e.g. filtering), we first need to -// remove the item with the greater index so indexes of other items (that are to -// be removed from the same array) do not shift. so below is for comparing 2 -// globs if they represent 2 items from the same array. -// example items from same array: ![*][2] ![0][*] ![0][1] ![0][3] -// should be sorted as ![0][3] ![*][2] ![0][1] ![0][*] - - -function _compareArrayItemGlobs(a, b) { - var reANote = re.ARRAY_GLOB_NOTE; // both should be negated - - if (!a.isNegated || !b.isNegated // should be same length (since we're comparing for items in same - // array) - || a.notes.length !== b.notes.length // last notes should be array brackets - || !reANote.test(a.last) || !reANote.test(b.last) // last notes should be different to compare - || a.last === b.last) return 0; // negated !..[*] should come last - - if (a.last === '[*]') return 1; // b is first - - if (b.last === '[*]') return -1; // a is first - - if (a.parent && b.parent) { - var _covers = Glob._covers; - - if (_covers(a.parent, b.parent, true)) { - return _compArrIdx(a.last, b.last); - } - - return 0; - } - - return _compArrIdx(a.last, b.last); -} // x vs !x.*.* » false -// x vs !x[*] » true -// x[*] vs !x » true -// x[*] vs !x[*] » false -// x.* vs !x.* » false - - -function _isReverseOf(a, b) { - return a.isNegated !== b.isNegated && a.absGlob === b.absGlob; -} - -function _invert(glob) { - return glob[0] === '!' ? glob.slice(1) : '!' + glob; -} - -var _rx = /^\s*!/; - -function _negFirstSort(a, b) { - var negA = _rx.test(a); - - var negB = _rx.test(b); - - if (negA && negB) return a.length >= b.length ? 1 : -1; - if (negA) return -1; - if (negB) return 1; - return 0; -} - -function _negLastSort(a, b) { - var negA = _rx.test(a); - - var negB = _rx.test(b); - - if (negA && negB) return a.length >= b.length ? 1 : -1; - if (negA) return 1; - if (negB) return -1; - return 0; -} // -------------------------------- -// EXPORT -// -------------------------------- - - - - -/***/ }), - -/***/ "./src/core/notation.js": -/*!******************************!*\ - !*** ./src/core/notation.js ***! - \******************************/ -/*! exports provided: Notation */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Notation", function() { return Notation; }); -/* harmony import */ var _notation_glob__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./notation.glob */ "./src/core/notation.glob.js"); -/* harmony import */ var _notation_error__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./notation.error */ "./src/core/notation.error.js"); -/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../utils */ "./src/utils.js"); -function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } - -function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } - -function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } - -/* eslint no-use-before-define:0, consistent-return:0, max-statements:0, max-len:0 */ - - - -var ERR = { - SOURCE: 'Invalid source. Expected a data object or array.', - DEST: 'Invalid destination. Expected a data object or array.', - NOTATION: 'Invalid notation: ', - NOTA_OBJ: 'Invalid notations object. ', - NO_INDEX: 'Implied index does not exist: ', - NO_PROP: 'Implied property does not exist: ' -}; // created test @ https://regex101.com/r/vLE16M/2 - -var reMATCHER = /(\[(\d+|".*"|'.*'|`.*`)\]|[a-z$_][a-z$_\d]*)/gi; // created test @ https://regex101.com/r/fL3PJt/1/ -// /^([a-z$_][a-z$_\d]*|\[(\d+|".*"|'.*'|`.*`)\])(\[(\d+|".*"|'.*'|`.*`)\]|(\.[a-z$_][a-z$_\d]*))*$/i - -var reVALIDATOR = new RegExp('^(' + '[a-z$_][a-z$_\\d]*' // JS variable syntax -+ '|' // OR -+ '\\[(\\d+|".*"|\'.*\')\\]' // array index or object bracket notation -+ ')' // exactly once -+ '(' + '\\[(\\d+|".*"|\'.*\')\\]' // followed by same -+ '|' // OR -+ '\\.[a-z$_][a-z$_\\d]*' // dot, then JS variable syntax -+ ')*' // (both) may repeat any number of times -+ '$', 'i'); -var DEFAULT_OPTS = Object.freeze({ - strict: false, - preserveIndices: false -}); -/** - * Notation.js for Node and Browser. - * - * Like in most programming languages, JavaScript makes use of dot-notation to - * access the value of a member of an object (or class). `Notation` class - * provides various methods for modifying / processing the contents of the - * given object; by parsing object notation strings or globs. - * - * Note that this class will only deal with enumerable properties of the source - * object; so it should be used to manipulate data objects. It will not deal - * with preserving the prototype-chain of the given object. - * - * @author Onur Yıldırım - * @license MIT - */ - -var Notation = /*#__PURE__*/function () { - /** - * Initializes a new instance of `Notation`. - * - * @param {Object|Array} [source={}] - The source object (or array) to be - * notated. Can either be an array or object. If omitted, defaults to an - * empty object. - * @param {Object} [options] - Notation options. - * @param {Boolean} [options.strict=false] - Whether to throw either when - * a notation path does not exist on the source (i.e. `#get()` and `#remove()` - * methods); or notation path exists but overwriting is disabled (i.e. - * `#set()` method). (Note that `.inspectGet()` and `.inspectRemove()` methods - * are exceptions). It's recommended to set this to `true` and prevent silent - * failures if you're working with sensitive data. Regardless of `strict` option, - * it will always throw on invalid notation syntax or other crucial failures. - * - * @example - * const obj = { car: { brand: "Dodge", model: "Charger", year: 1970 } }; - * const notation = new Notation(obj); - * notation.get('car.model') // » "Charger" - * notation.remove('car.model').set('car.color', 'red').value - * // » { car: { brand: "Dodge", year: 1970, color: "red" } } - */ - function Notation(source, options) { - _classCallCheck(this, Notation); - - if (arguments.length === 0) { - this._source = {}; - } else if (!_utils__WEBPACK_IMPORTED_MODULE_2__["utils"].isCollection(source)) { - throw new _notation_error__WEBPACK_IMPORTED_MODULE_1__["NotationError"](ERR.SOURCE); - } else { - this._source = source; - } - - this._isArray = _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].type(this._source) === 'array'; - this.options = options; - } // -------------------------------- - // INSTANCE PROPERTIES - // -------------------------------- - - /** - * Gets or sets notation options. - * @type {Object} - */ - - - _createClass(Notation, [{ - key: "each", - // -------------------------------- - // INSTANCE METHODS - // -------------------------------- - - /** - * Recursively iterates through each key of the source object and invokes - * the given callback function with parameters, on each non-object value. - * - * @param {Function} callback - The callback function to be invoked on - * each on each non-object value. To break out of the loop, return `false` - * from within the callback. - * Callback signature: `callback(notation, key, value, object) { ... }` - * - * @returns {Notation} - The current `Notation` instance (self). - * - * @example - * const obj = { car: { brand: "Dodge", model: "Charger", year: 1970 } }; - * Notation.create(obj).each(function (notation, key, value, object) { - * console.log(notation, value); - * }); - * // "car.brand" "Dodge" - * // "car.model" "Charger" - * // "car.year" 1970 - */ - value: function each(callback) { - _each(this._source, callback); - - return this; - } - /** - * Iterates through each note of the given notation string by evaluating - * it on the source object. - * - * @param {String} notation - The notation string to be iterated through. - * @param {Function} callback - The callback function to be invoked on - * each iteration. To break out of the loop, return `false` from within - * the callback. Signature: `callback(levelValue, note, index, list)` - * - * @returns {Notation} - The current `Notation` instance (self). - * - * @example - * const obj = { car: { brand: "Dodge", model: "Charger", year: 1970 } }; - * Notation.create(obj) - * .eachValue("car.brand", function (levelValue, note, index, list) { - * console.log(note, levelValue); // "car.brand" "Dodge" - * }); - */ - - }, { - key: "eachValue", - value: function eachValue(notation, callback) { - var level = this._source; - Notation.eachNote(notation, function (levelNotation, note, index, list) { - level = _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].hasOwn(level, note) ? level[note] : undefined; - if (callback(level, levelNotation, note, index, list) === false) return false; - }); - return this; - } - /** - * Gets the list of notations from the source object (keys). - * - * @returns {Array} - An array of notation strings. - * - * @example - * const obj = { car: { brand: "Dodge", model: "Charger", year: 1970 } }; - * const notations = Notation.create(obj).getNotations(); - * console.log(notations); // [ "car.brand", "car.model", "car.year" ] - */ - - }, { - key: "getNotations", - value: function getNotations() { - var list = []; - this.each(function (notation) { - list.push(notation); - }); - return list; - } - /** - * Deeply clones the source object. This is also useful if you want to - * prevent mutating the original source object. - * - *
- * Note that `Notation` expects a data object (or array) with enumerable - * properties. In addition to plain objects and arrays; supported cloneable - * property/value types are primitives (such as `String`, `Number`, - * `Boolean`, `Symbol`, `null` and `undefined`) and built-in types (such as - * `Date` and `RegExp`). - * - * Enumerable properties with types other than these (such as methods, - * special objects, custom class instances, etc) will be copied by reference. - * Non-enumerable properties will not be cloned. - * - * If you still need full clone support, you can use a library like lodash. - * e.g. `Notation.create(_.cloneDeep(source))` - *
- * - * @returns {Notation} - The current `Notation` instance (self). - * - * @example - * const mutated = Notation.create(source1).set('newProp', true).value; - * console.log(source1.newProp); // ——» true - * - * const cloned = Notation.create(source2).clone().set('newProp', true).value; - * console.log('newProp' in source2); // ——» false - * console.log(cloned.newProp); // ——» true - */ - - }, { - key: "clone", - value: function clone() { - this._source = _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].cloneDeep(this._source); - return this; - } - /** - * Flattens the source object to a single-level object with notated keys. - * - * @returns {Notation} - The current `Notation` instance (self). - * - * @example - * const obj = { car: { brand: "Dodge", model: "Charger", year: 1970 } }; - * console.log(Notation.create(obj).flatten().value); - * // { - * // "car.brand": "Dodge", - * // "car.model": "Charger", - * // "car.year": 1970 - * // } - */ - - }, { - key: "flatten", - value: function flatten() { - var o = {}; - this.each(function (notation, key, value) { - o[notation] = value; - }); - this._source = o; - return this; - } - /** - * Aggregates notated keys of a (single-level) object, and nests them under - * their corresponding properties. This is the opposite of `Notation#flatten` - * method. This might be useful when expanding a flat object fetched from - * a database. - * @alias Notation#aggregate - * @chainable - * - * @returns {Notation} - The current `Notation` instance (self). - * - * @example - * const obj = { "car.brand": "Dodge", "car.model": "Charger", "car.year": 1970 } - * const expanded = Notation.create(obj).expand().value; - * console.log(expanded); // { car: { brand: "Dodge", model: "Charger", year: 1970 } }; - */ - - }, { - key: "expand", - value: function expand() { - this._source = Notation.create({}).merge(this._source).value; - return this; - } - /** - * Alias for `#expand` - * @private - * @returns {Notation} - - */ - - }, { - key: "aggregate", - value: function aggregate() { - return this.expand(); - } - /** - * Inspects the given notation on the source object by checking - * if the source object actually has the notated property; - * and getting its value if exists. - * @param {String} notation - The notation string to be inspected. - * @returns {InspectResult} - The result object. - * - * @example - * Notation.create({ car: { year: 1970 } }).inspectGet("car.year"); - * // { has: true, value: 1970, lastNote: 'year', lastNoteNormalized: 'year' } - * Notation.create({ car: { year: 1970 } }).inspectGet("car.color"); - * // { has: false } - * Notation.create({ car: { color: undefined } }).inspectGet("car.color"); - * // { has: true, value: undefined, lastNote: 'color', lastNoteNormalized: 'color' } - * Notation.create({ car: { brands: ['Ford', 'Dodge'] } }).inspectGet("car.brands[1]"); - * // { has: true, value: 'Dodge', lastNote: '[1]', lastNoteNormalized: 1 } - */ - - }, { - key: "inspectGet", - value: function inspectGet(notation) { - var level = this._source; - var result = { - has: false, - value: undefined - }; - var parent; - Notation.eachNote(notation, function (levelNotation, note, index) { - var lastNoteNormalized = _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].normalizeNote(note); - - if (_utils__WEBPACK_IMPORTED_MODULE_2__["utils"].hasOwn(level, lastNoteNormalized)) { - level = level[lastNoteNormalized]; - parent = level; - result = { - notation: notation, - has: true, - value: level, - type: _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].type(level), - level: index + 1, - lastNote: note, - lastNoteNormalized: lastNoteNormalized - }; - } else { - // level = undefined; - result = { - notation: notation, - has: false, - type: 'undefined', - level: index + 1, - lastNote: note, - lastNoteNormalized: lastNoteNormalized - }; - return false; // break out - } - }); - if (parent === undefined || result.has && parent === result.value) parent = this._source; - result.parentIsArray = _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].type(parent) === 'array'; - return result; - } - /** - * Notation inspection result object. - * @typedef Notation~InspectResult - * @type Object - * @property {String} notation - Notation that is inspected. - * @property {Boolean} has - Indicates whether the source object has the - * given notation as a (leveled) enumerable property. If the property - * exists but has a value of `undefined`, this will still return `true`. - * @property {*} value - The value of the notated property. If the source - * object does not have the notation, the value will be `undefined`. - * @property {String} type - The type of the notated property. If the source - * object does not have the notation, the type will be `"undefined"`. - * @property {String} lastNote - Last note of the notation, if actually - * exists. For example, last note of `'a.b.c'` is `'c'`. - * @property {String|Number} lastNoteNormalized - Normalized representation - * of the last note of the notation, if actually exists. For example, last - * note of `'a.b[1]` is `'[1]'` and will be normalized to number `1`; which - * indicates an array index. - * @property {Boolean} parentIsArray - Whether the parent object of the - * notation path is an array. - */ - - /** - * Inspects and removes the given notation from the source object by - * checking if the source object actually has the notated property; and - * getting its value if exists, before removing the property. - * - * @param {String} notation - The notation string to be inspected. - * - * @returns {InspectResult} - The result object. - * - * @example - * const obj = { name: "John", car: { year: 1970 } }; - * let result = Notation.create(obj).inspectRemove("car.year"); - * // result » { notation: "car.year", has: true, value: 1970, lastNote: "year", lastNoteNormalized: "year" } - * // obj » { name: "John", car: {} } - * - * result = Notation.create({ car: { year: 1970 } }).inspectRemove("car.color"); - * // result » { notation: "car.color", has: false } - * Notation.create({ car: { color: undefined } }).inspectRemove("car['color']"); - * // { notation: "car.color", has: true, value: undefined, lastNote: "['color']", lastNoteNormalized: "color" } - * - * const obj = { car: { colors: ["black", "white"] } }; - * const result = Notation.create().inspectRemove("car.colors[0]"); - * // result » { notation: "car.colors[0]", has: true, value: "black", lastNote: "[0]", lastNoteNormalized: 0 } - * // obj » { car: { colors: [(empty), "white"] } } - */ - - }, { - key: "inspectRemove", - value: function inspectRemove(notation) { - if (!notation) throw new Error(ERR.NOTATION + "'".concat(notation, "'")); - var parentNotation = Notation.parent(notation); - var parent = parentNotation ? this.get(parentNotation, null) : this._source; - var parentIsArray = _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].type(parent) === 'array'; - var notes = Notation.split(notation); - var lastNote = notes[notes.length - 1]; - var lastNoteNormalized = _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].normalizeNote(lastNote); - var result, value; - - if (_utils__WEBPACK_IMPORTED_MODULE_2__["utils"].hasOwn(parent, lastNoteNormalized)) { - value = parent[lastNoteNormalized]; - result = { - notation: notation, - has: true, - value: value, - type: _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].type(value), - level: notes.length, - lastNote: lastNote, - lastNoteNormalized: lastNoteNormalized, - parentIsArray: parentIsArray - }; // if `preserveIndices` is enabled and this is an array, we'll - // splice the item out. otherwise, we'll use `delete` syntax to - // empty the item. - - if (!this.options.preserveIndices && parentIsArray) { - parent.splice(lastNoteNormalized, 1); - } else { - delete parent[lastNoteNormalized]; - } - } else { - result = { - notation: notation, - has: false, - type: 'undefined', - level: notes.length, - lastNote: lastNote, - lastNoteNormalized: lastNoteNormalized, - parentIsArray: parentIsArray - }; - } - - return result; - } - /** - * Checks whether the source object has the given notation - * as a (leveled) enumerable property. If the property exists - * but has a value of `undefined`, this will still return `true`. - * @param {String} notation - The notation string to be checked. - * @returns {Boolean} - - * - * @example - * Notation.create({ car: { year: 1970 } }).has("car.year"); // true - * Notation.create({ car: { year: undefined } }).has("car.year"); // true - * Notation.create({}).has("car.color"); // false - */ - - }, { - key: "has", - value: function has(notation) { - return this.inspectGet(notation).has; - } - /** - * Checks whether the source object has the given notation - * as a (leveled) defined enumerable property. If the property - * exists but has a value of `undefined`, this will return `false`. - * @param {String} notation - The notation string to be checked. - * @returns {Boolean} - - * - * @example - * Notation.create({ car: { year: 1970 } }).hasDefined("car.year"); // true - * Notation.create({ car: { year: undefined } }).hasDefined("car.year"); // false - * Notation.create({}).hasDefined("car.color"); // false - */ - - }, { - key: "hasDefined", - value: function hasDefined(notation) { - return this.inspectGet(notation).value !== undefined; - } - /** - * Gets the value of the corresponding property at the given notation. - * - * @param {String} notation - The notation string to be processed. - * @param {String} [defaultValue] - The default value to be returned if the - * property is not found or enumerable. - * - * @returns {*} - The value of the notated property. - * @throws {NotationError} - If `strict` option is enabled, `defaultValue` - * is not set and notation does not exist. - * - * @example - * Notation.create({ car: { brand: "Dodge" } }).get("car.brand"); // "Dodge" - * Notation.create({ car: {} }).get("car.model", "Challenger"); // "Challenger" - * Notation.create({ car: { model: undefined } }).get("car.model", "Challenger"); // undefined - * - * @example get value when strict option is enabled - * // strict option defaults to false - * Notation.create({ car: {} }).get("car.model"); // undefined - * Notation.create({ car: {} }, { strict: false }).get("car.model"); // undefined - * // below will throw bec. strict = true, car.model does not exist - * // and no default value is given. - * Notation.create({ car: {} }, { strict: true }).get("car.model"); - */ - - }, { - key: "get", - value: function get(notation, defaultValue) { - var result = this.inspectGet(notation); // if strict and no default value is set, check if implied index or prop - // exists - - if (this.options.strict && arguments.length < 2 && !result.has) { - var msg = result.parentIsArray ? ERR.NO_INDEX : ERR.NO_PROP; - throw new _notation_error__WEBPACK_IMPORTED_MODULE_1__["NotationError"](msg + "'".concat(notation, "'")); - } - - return result.has ? result.value : defaultValue; - } - /** - * Sets the value of the corresponding property at the given notation. If - * the property does not exist, it will be created and nested at the - * calculated level. If it exists; its value will be overwritten by - * default. - * @chainable - * - * @param {String} notation - The notation string to be processed. - * @param {*} value - The value to be set for the notated property. - * @param {String|Boolean} [mode="overwrite"] - Write mode. By default, - * this is set to `"overwrite"` which sets the value by overwriting the - * target object property or array item at index. To insert an array item - * (by shifting the index, instead of overwriting); set to `"insert"`. To - * prevent overwriting the value if exists, explicitly set to `false`. - * - * @returns {Notation} - The current `Notation` instance (self). - * - * @throws {NotationError} - If strict notation is enabled, `overwrite` - * option is set to `false` and attempted to overwrite an existing value. - * - * @example - * const obj = { car: { brand: "Dodge", year: 1970 } }; - * Notation.create(obj) - * .set("car.brand", "Ford") - * .set("car.model", "Mustang") - * .set("car.year", 1965, false) - * .set("car.color", "red") - * .set("boat", "none"); - * console.log(obj); - * // { notebook: "Mac", car: { brand: "Ford", model: "Mustang", year: 1970, color: "red" }, boat: "none" }; - */ - - }, { - key: "set", - value: function set(notation, value) { - var mode = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'overwrite'; - if (!notation.trim()) throw new _notation_error__WEBPACK_IMPORTED_MODULE_1__["NotationError"](ERR.NOTATION + "'".concat(notation, "'")); - if (mode === true) mode = 'overwrite'; - var level = this._source; - var currentIsLast, nCurrentNote, nNextNote, nextIsArrayNote, type; - var insertErrMsg = 'Cannot set value by inserting at index, on an object'; - Notation.eachNote(notation, function (levelNotation, note, index, list) { - currentIsLast = index === list.length - 1; - nCurrentNote = nNextNote || _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].normalizeNote(note); - nNextNote = currentIsLast ? null : _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].normalizeNote(list[index + 1]); - type = _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].type(level); - - if (type === 'array' && typeof nCurrentNote !== 'number') { - var parent = Notation.parent(levelNotation) || 'source'; - throw new _notation_error__WEBPACK_IMPORTED_MODULE_1__["NotationError"]("Cannot set string key '".concat(note, "' on array ").concat(parent)); - } // check if the property is at this level - - - if (_utils__WEBPACK_IMPORTED_MODULE_2__["utils"].hasOwn(level, nCurrentNote, type)) { - // check if we're at the last level - if (currentIsLast) { - // if mode is "overwrite", assign the value. - if (mode === 'overwrite') { - level[nCurrentNote] = value; - } else if (mode === 'insert') { - if (type === 'array') { - level.splice(nCurrentNote, 0, value); - } else { - throw new _notation_error__WEBPACK_IMPORTED_MODULE_1__["NotationError"](insertErrMsg); - } - } // otherwise, will not overwrite - - } else { - // if not last level; just re-reference the current level. - level = level[nCurrentNote]; - } - } else { - if (currentIsLast && type !== 'array' && mode === 'insert') { - throw new _notation_error__WEBPACK_IMPORTED_MODULE_1__["NotationError"](insertErrMsg); - } // if next normalized note is a number, it indicates that the - // current note is actually an array. - - - nextIsArrayNote = typeof nNextNote === 'number'; // we don't have this property at this level so; if this is the - // last level, we set the value if not, we set an empty - // collection for the next level - - level[nCurrentNote] = currentIsLast ? value : nextIsArrayNote ? [] : {}; - level = level[nCurrentNote]; - } - }); - return this; - } - /** - * Just like the `.set()` method but instead of a single notation - * string, an object of notations and values can be passed. - * Sets the value of each corresponding property at the given - * notation. If a property does not exist, it will be created - * and nested at the calculated level. If it exists; its value - * will be overwritten by default. - * @chainable - * - * @param {Object} notationsObject - The notations object to be processed. - * This can either be a regular object with non-dotted keys - * (which will be merged to the first/root level of the source object); - * or a flattened object with notated (dotted) keys. - * @param {Boolean} [overwrite=true] - Whether to overwrite a property if - * exists. - * - * @returns {Notation} - The current `Notation` instance (self). - * - * @example - * const obj = { car: { brand: "Dodge", year: 1970 } }; - * Notation.create(obj).merge({ - * "car.brand": "Ford", - * "car.model": "Mustang", - * "car.year": 1965, - * "car.color": "red", - * "boat": "none" - * }); - * console.log(obj); - * // { car: { brand: "Ford", model: "Mustang", year: 1970, color: "red" }, boat: "none" }; - */ - - }, { - key: "merge", - value: function merge(notationsObject) { - var _this = this; - - var overwrite = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; - - if (_utils__WEBPACK_IMPORTED_MODULE_2__["utils"].type(notationsObject) !== 'object') { - throw new _notation_error__WEBPACK_IMPORTED_MODULE_1__["NotationError"](ERR.NOTA_OBJ + 'Expected an object.'); - } - - var value; - _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].each(Object.keys(notationsObject), function (notation) { - value = notationsObject[notation]; - - _this.set(notation, value, overwrite); - }); - return this; - } - /** - * Removes the properties by the given list of notations from the source - * object and returns a new object with the removed properties. - * Opposite of `merge()` method. - * - * @param {Array} notations - The notations array to be processed. - * - * @returns {Object} - An object with the removed properties. - * - * @example - * const obj = { car: { brand: "Dodge", year: 1970 }, notebook: "Mac" }; - * const separated = Notation.create(obj).separate(["car.brand", "boat" ]); - * console.log(separated); - * // { notebook: "Mac", car: { brand: "Ford" } }; - * console.log(obj); - * // { car: { year: 1970 } }; - */ - - }, { - key: "separate", - value: function separate(notations) { - var _this2 = this; - - if (_utils__WEBPACK_IMPORTED_MODULE_2__["utils"].type(notations) !== 'array') { - throw new _notation_error__WEBPACK_IMPORTED_MODULE_1__["NotationError"](ERR.NOTA_OBJ + 'Expected an array.'); - } - - var o = new Notation({}); - _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].each(notations, function (notation) { - var result = _this2.inspectRemove(notation); - - o.set(notation, result.value); - }); - this._source = o._source; - return this; - } - /** - * Deep clones the source object while filtering its properties by the - * given glob notations. Includes all matched properties and removes - * the rest. - * - * The difference between regular notations and glob-notations is that; - * with the latter, you can use wildcard stars (*) and negate the notation - * by prepending a bang (!). A negated notation will be excluded. - * - * Order of the globs does not matter; they will be logically sorted. Loose - * globs will be processed first and verbose globs or normal notations will - * be processed last. e.g. `[ "car.model", "*", "!car.*" ]` will be - * normalized and sorted as `[ "*", "!car" ]`. - * - * Passing no parameters or passing a glob of `"!*"` or `["!*"]` will empty - * the source object. See `Notation.Glob` class for more information. - * @chainable - * - * @param {Array|String} globList - Glob notation list to be processed. - * @param {Object} [options] - Filter options. - * @param {Boolean} [options.restrictive=false] - Whether negated items - * strictly remove every match. Note that, regardless of this option, if - * any item has an exact negated version; non-negated is always removed. - * - * @returns {Notation} - The current `Notation` instance (self). To get the - * filtered value, call `.value` property on the instance. - * - * @example - * const car = { brand: "Ford", model: { name: "Mustang", year: 1970 } }; - * const n = Notation.create(car); - * - * console.log(n.filter([ "*", "!model.year" ]).value); // { brand: "Ford", model: { name: "Mustang" } } - * console.log(n.filter("model.name").value); // { model: { name: "Mustang" } } - * console.log(car); // { brand: "Ford", model: { name: "Mustang", year: 1970 } } - * console.log(n.filter().value); // {} // —» equivalent to n.filter("") or n.filter("!*") - */ - - }, { - key: "filter", - value: function filter(globList) { - var _this3 = this; - - var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - var re = _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].re; // ensure array, normalize and sort the globs in logical order. this - // also concats the array first (to prevent mutating the original - // array). - - var globs = _notation_glob__WEBPACK_IMPORTED_MODULE_0__["Glob"].normalize(globList, options.restrictive); - var len = globs.length; - var empty = this._isArray ? [] : {}; // if globs is "" or [""] or ["!*"] or ["![*]"] set source to empty and return. - - if (len === 0 || len === 1 && (!globs[0] || re.NEGATE_ALL.test(globs[0]))) { - this._source = empty; - return this; - } - - var cloned = _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].cloneDeep(this.value); - var firstIsWildcard = re.WILDCARD.test(globs[0]); // if globs only consist of "*" or "[*]"; set the "clone" as source and - // return. - - if (len === 1 && firstIsWildcard) { - this._source = cloned; - return this; - } - - var filtered; // if the first item of sorted globs is "*" or "[*]" we set the source - // to the (full) "copy" and remove the wildcard from globs (not to - // re-process). - - if (firstIsWildcard) { - filtered = new Notation(cloned); - globs.shift(); - } else { - // otherwise we set an empty object or array as the source so that - // we can add notations/properties to it. - filtered = new Notation(empty); - } // iterate through globs - - - _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].each(globs, function (globNotation) { - // console.log('globNotation', globNotation); - var g = new _notation_glob__WEBPACK_IMPORTED_MODULE_0__["Glob"](globNotation); - var glob = g.glob, - absGlob = g.absGlob, - isNegated = g.isNegated, - levels = g.levels; - var normalized, emptyValue, eType; // check whether the glob ends with `.*` or `[*]` then remove - // trailing glob note and decide for empty value (if negated). for - // non-negated, trailing wildcards are already removed by - // normalization. - - if (absGlob.slice(-2) === '.*') { - normalized = absGlob.slice(0, -2); - /* istanbul ignore else */ - - if (isNegated) emptyValue = {}; - eType = 'object'; - } else if (absGlob.slice(-3) === '[*]') { - normalized = absGlob.slice(0, -3); - /* istanbul ignore else */ - - if (isNegated) emptyValue = []; - eType = 'array'; - } else { - normalized = absGlob; - } // we'll check glob vs value integrity if emptyValue is set; and throw if needed. - - - var errGlobIntegrity = "Integrity failed for glob '".concat(glob, "'. Cannot set empty ").concat(eType, " for '").concat(normalized, "' which has a type of "); // ... - // check if remaining normalized glob has no wildcard stars e.g. - // "a.b" or "!a.b.c" etc.. - - if (re.WILDCARDS.test(normalized) === false) { - if (isNegated) { - // inspect and directly remove the notation if negated. - // we need the inspection for the detailed error below. - var insRemove = filtered.inspectRemove(normalized); // console.log('insRemove', insRemove); - // if original glob had `.*` at the end, it means remove - // contents (not itself). so we'll set an empty object. - // meaning `some.prop` (prop) is removed completely but - // `some.prop.*` (prop) results in `{}`. For array notation - // (`[*]`), we'll set an empty array. - - if (emptyValue) { - // e.g. for glob `![0].x.*` we expect to set `[0].x = {}` - // but if `.x` is not an object (or array), we should fail. - var vType = insRemove.type; - var errMsg = errGlobIntegrity + "'".concat(vType, "'."); // in non-strict mode, only exceptions are `null` and - // `undefined`, for which we won't throw but we'll not - // set an empty obj/arr either. - - var isValSet = _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].isset(insRemove.value); // on critical type mismatch we throw - // or if original value is undefined or null in strict mode we throw - - if (isValSet && vType !== eType || !isValSet && _this3.options.strict) { - throw new _notation_error__WEBPACK_IMPORTED_MODULE_1__["NotationError"](errMsg); - } // if parent is an array, we'll insert the value at - // index bec. we've removed the item and indexes are - // shifted. Otherwise, we'll simply overwrite the - // object property value. - - - var setMode = insRemove.parentIsArray ? 'insert' : 'overwrite'; // console.log('setting', normalized, emptyValue, setMode); - - filtered.set(normalized, emptyValue, setMode); - } - } else { - // directly set the same notation from the original - var insGet = _this3.inspectGet(normalized); // Notation.create(original).inspectGet ... - - /* istanbul ignore else */ - - - if (insGet.has) filtered.set(normalized, insGet.value, 'overwrite'); - } // move to the next - - - return true; - } // if glob has wildcard(s), we'll iterate through keys of the source - // object and see if (full) notation of each key matches the current - // glob. - // important! we will iterate with eachRight to prevent shifted - // indexes when removing items from arrays. - - - var reverseIterateIfArray = true; - - _each(_this3._source, function (originalNotation, key, value) { - var originalIsCovered = _notation_glob__WEBPACK_IMPORTED_MODULE_0__["Glob"].create(normalized).covers(originalNotation); // console.log('» normalized:', normalized, 'covers', originalNotation, '»', originalIsCovered); - - if (!originalIsCovered) return true; // break - - if (_this3.options.strict && emptyValue) { - // since original is covered and we have emptyValue set (due - // to trailing wildcard), here we'll check value vs glob - // integrity; (only if we're in strict mode). - var _vType = _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].type(value); // types and number of levels are the same? - - - if (_vType !== eType // we subtract 1 from number of levels bec. the last - // note is removed since we have emptyValue set. - && Notation.split(originalNotation).length === levels.length - 1) { - throw new _notation_error__WEBPACK_IMPORTED_MODULE_1__["NotationError"](errGlobIntegrity + "'".concat(_vType, "'.")); - } - } // iterating each note of original notation. i.e.: - // note1.note2.note3 is iterated from left to right, as: - // 'note1', 'note1.note2', 'note1.note2.note3' — in order. - - - Notation.eachNote(originalNotation, function (levelNotation) { - // console.log(' level »', glob, 'covers', levelNotation, '»', g.test(levelNotation)); - if (g.test(levelNotation)) { - var levelLen = Notation.split(levelNotation).length; - /* istanbul ignore else */ - - if (isNegated && levels.length <= levelLen) { - // console.log(' » removing', levelNotation, 'of', originalNotation); - filtered.remove(levelNotation); // we break and return early if removed bec. e.g. - // when 'note1.note2' (parent) of - // 'note1.note2.note3' is also removed, we no more - // have 'note3'. - - return false; - } // console.log(' » setting', levelNotation, '=', value); - - - filtered.set(levelNotation, value, 'overwrite'); - } - }); - }, reverseIterateIfArray); - }); // finally set the filtered's value as the source of our instance and - // return. - - this._source = filtered.value; - return this; - } - /** - * Removes the property from the source object, at the given notation. - * @alias Notation#delete - * @chainable - * @param {String} notation - The notation to be inspected. - * @returns {Notation} - The current `Notation` instance (self). - * @throws {NotationError} - If `strict` option is enabled and notation - * does not exist. - * - * @example - * const obj = { notebook: "Mac", car: { model: "Mustang" } }; - * Notation.create(obj).remove("car.model"); - * console.log(obj); // { notebook: "Mac", car: { } } - */ - - }, { - key: "remove", - value: function remove(notation) { - var result = this.inspectRemove(notation); // if strict, check if implied index or prop exists - - if (this.options.strict && !result.has) { - var msg = result.parentIsArray ? ERR.NO_INDEX : ERR.NO_PROP; - throw new _notation_error__WEBPACK_IMPORTED_MODULE_1__["NotationError"](msg + "'".concat(notation, "'")); - } - - return this; - } - /** - * Alias of `Notation#remove` - * @private - * @param {String} notation - - * @returns {Notation} - - */ - - }, { - key: "delete", - value: function _delete(notation) { - this.remove(notation); - return this; - } - /** - * Copies the notated property from the source collection and adds it to the - * destination — only if the source object actually has that property. - * This is different than a property with a value of `undefined`. - * @chainable - * - * @param {Object|Array} destination - The destination object that the notated - * properties will be copied to. - * @param {String} notation - The notation to get the corresponding property - * from the source object. - * @param {String} [newNotation=null] - The notation to set the source property - * on the destination object. In other words, the copied property will be - * renamed to this value before set on the destination object. If not set, - * `notation` argument will be used. - * @param {Boolean} [overwrite=true] - Whether to overwrite the property on - * the destination object if it exists. - * - * @returns {Notation} - The current `Notation` instance (self). - * - * @throws {NotationError} - If `destination` is not a valid collection. - * @throws {NotationError} - If `notation` or `newNotation` is invalid. - * - * @example - * const obj = { car: { brand: "Ford", model: "Mustang" } }; - * const models = { dodge: "Charger" }; - * Notation.create(obj).copyTo(models, "car.model", "ford"); - * console.log(models); - * // { dodge: "Charger", ford: "Mustang" } - * // source object (obj) is not modified - */ - - }, { - key: "copyTo", - value: function copyTo(destination, notation) { - var newNotation = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; - var overwrite = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true; - if (!_utils__WEBPACK_IMPORTED_MODULE_2__["utils"].isCollection(destination)) throw new _notation_error__WEBPACK_IMPORTED_MODULE_1__["NotationError"](ERR.DEST); - var result = this.inspectGet(notation); - - if (result.has) { - var newN = _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].getNewNotation(newNotation, notation); - Notation.create(destination).set(newN, result.value, overwrite); - } - - return this; - } - /** - * Copies the notated property from the target collection and adds it to - * (own) source object — only if the target object actually has that - * property. This is different than a property with a value of `undefined`. - * @chainable - * - * @param {Object|Array} target - The target collection that the notated - * properties will be copied from. - * @param {String} notation - The notation to get the corresponding - * property from the target object. - * @param {String} [newNotation=null] - The notation to set the copied - * property on our source collection. In other words, the copied property - * will be renamed to this value before set. If not set, `notation` - * argument will be used. - * @param {Boolean} [overwrite=true] - Whether to overwrite the property on - * our collection if it exists. - * - * @returns {Notation} - The current `Notation` instance (self). - * - * @throws {NotationError} - If `target` is not a valid collection. - * @throws {NotationError} - If `notation` or `newNotation` is invalid. - * - * @example - * const obj = { car: { brand: "Ford", model: "Mustang" } }; - * const models = { dodge: "Charger" }; - * Notation.create(obj).copyFrom(models, "dodge", "car.model", true); - * console.log(obj); - * // { car: { brand: "Ford", model: "Charger" } } - * // models object is not modified - */ - - }, { - key: "copyFrom", - value: function copyFrom(target, notation) { - var newNotation = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; - var overwrite = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true; - if (!_utils__WEBPACK_IMPORTED_MODULE_2__["utils"].isCollection(target)) throw new _notation_error__WEBPACK_IMPORTED_MODULE_1__["NotationError"](ERR.DEST); - var result = Notation.create(target).inspectGet(notation); - - if (result.has) { - var newN = _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].getNewNotation(newNotation, notation); - this.set(newN, result.value, overwrite); - } - - return this; - } - /** - * Removes the notated property from the source (own) collection and adds - * it to the destination — only if the source collection actually has that - * property. This is different than a property with a value of `undefined`. - * @chainable - * - * @param {Object|Array} destination - The destination collection that the - * notated properties will be moved to. - * @param {String} notation - The notation to get the corresponding - * property from the source object. - * @param {String} [newNotation=null] - The notation to set the source - * property on the destination object. In other words, the moved property - * will be renamed to this value before set on the destination object. If - * not set, `notation` argument will be used. - * @param {Boolean} [overwrite=true] - Whether to overwrite the property on - * the destination object if it exists. - * - * @returns {Notation} - The current `Notation` instance (self). - * - * @throws {NotationError} - If `destination` is not a valid collection. - * @throws {NotationError} - If `notation` or `newNotation` is invalid. - * - * @example - * const obj = { car: { brand: "Ford", model: "Mustang" } }; - * const models = { dodge: "Charger" }; - * Notation.create(obj).moveTo(models, "car.model", "ford"); - * console.log(obj); - * // { car: { brand: "Ford" } } - * console.log(models); - * // { dodge: "Charger", ford: "Mustang" } - */ - - }, { - key: "moveTo", - value: function moveTo(destination, notation) { - var newNotation = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; - var overwrite = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true; - if (!_utils__WEBPACK_IMPORTED_MODULE_2__["utils"].isCollection(destination)) throw new _notation_error__WEBPACK_IMPORTED_MODULE_1__["NotationError"](ERR.DEST); - var result = this.inspectRemove(notation); - - if (result.has) { - var newN = _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].getNewNotation(newNotation, notation); - Notation.create(destination).set(newN, result.value, overwrite); - } - - return this; - } - /** - * Removes the notated property from the target collection and adds it to (own) - * source collection — only if the target object actually has that property. - * This is different than a property with a value of `undefined`. - * @chainable - * - * @param {Object|Array} target - The target collection that the notated - * properties will be moved from. - * @param {String} notation - The notation to get the corresponding property - * from the target object. - * @param {String} [newNotation=null] - The notation to set the target - * property on the source object. In other words, the moved property - * will be renamed to this value before set on the source object. - * If not set, `notation` argument will be used. - * @param {Boolean} [overwrite=true] - Whether to overwrite the property on - * the source object if it exists. - * - * @returns {Notation} - The current `Notation` instance (self). - * - * @throws {NotationError} - If `target` is not a valid collection. - * @throws {NotationError} - If `notation` or `newNotation` is invalid. - * - * @example - * const obj = { car: { brand: "Ford", model: "Mustang" } }; - * const models = { dodge: "Charger" }; - * Notation.create(obj).moveFrom(models, "dodge", "car.model", true); - * console.log(obj); - * // { car: { brand: "Ford", model: "Charger" } } - * console.log(models); - * // {} - */ - - }, { - key: "moveFrom", - value: function moveFrom(target, notation) { - var newNotation = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; - var overwrite = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true; - if (!_utils__WEBPACK_IMPORTED_MODULE_2__["utils"].isCollection(target)) throw new _notation_error__WEBPACK_IMPORTED_MODULE_1__["NotationError"](ERR.DEST); - var result = Notation.create(target).inspectRemove(notation); - - if (result.has) { - var newN = _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].getNewNotation(newNotation, notation); - this.set(newN, result.value, overwrite); - } - - return this; - } - /** - * Renames the notated property of the source collection by the new notation. - * @alias Notation#renote - * @chainable - * - * @param {String} notation - The notation to get the corresponding - * property (value) from the source collection. - * @param {String} newNotation - The new notation for the targeted - * property value. If not set, the source collection will not be modified. - * @param {Boolean} [overwrite=true] - Whether to overwrite the property at - * the new notation, if it exists. - * - * @returns {Notation} - The current `Notation` instance (self). - * - * @throws {NotationError} - If `notation` or `newNotation` is invalid. - * - * @example - * const obj = { car: { brand: "Ford", model: "Mustang" } }; - * Notation.create(obj) - * .rename("car.brand", "carBrand") - * .rename("car.model", "carModel"); - * console.log(obj); - * // { carBrand: "Ford", carModel: "Mustang" } - */ - - }, { - key: "rename", - value: function rename(notation, newNotation, overwrite) { - return this.moveTo(this._source, notation, newNotation, overwrite); - } - /** - * Alias for `#rename` - * @private - * @param {String} notation - - * @param {String} newNotation - - * @param {Boolean} [overwrite=true] - - * @returns {Notation} - - */ - - }, { - key: "renote", - value: function renote(notation, newNotation, overwrite) { - return this.rename(notation, newNotation, overwrite); - } - /** - * Extracts the property at the given notation to a new object by copying - * it from the source collection. This is equivalent to `.copyTo({}, - * notation, newNotation)`. - * @alias Notation#copyToNew - * - * @param {String} notation - The notation to get the corresponding - * property (value) from the source object. - * @param {String} newNotation - The new notation to be set on the new - * object for the targeted property value. If not set, `notation` argument - * will be used. - * - * @returns {Object} - Returns a new object with the notated property. - * - * @throws {NotationError} - If `notation` or `newNotation` is invalid. - * - * @example - * const obj = { car: { brand: "Ford", model: "Mustang" } }; - * const extracted = Notation.create(obj).extract("car.brand", "carBrand"); - * console.log(extracted); - * // { carBrand: "Ford" } - * // obj is not modified - */ - - }, { - key: "extract", - value: function extract(notation, newNotation) { - var o = {}; - this.copyTo(o, notation, newNotation); - return o; - } - /** - * Alias for `#extract` - * @private - * @param {String} notation - - * @param {String} newNotation - - * @returns {Object} - - */ - - }, { - key: "copyToNew", - value: function copyToNew(notation, newNotation) { - return this.extract(notation, newNotation); - } - /** - * Extrudes the property at the given notation to a new collection by - * moving it from the source collection. This is equivalent to `.moveTo({}, - * notation, newNotation)`. - * @alias Notation#moveToNew - * - * @param {String} notation - The notation to get the corresponding - * property (value) from the source object. - * @param {String} newNotation - The new notation to be set on the new - * object for the targeted property value. If not set, `notation` argument - * will be used. - * - * @returns {Object} - Returns a new object with the notated property. - * - * @example - * const obj = { car: { brand: "Ford", model: "Mustang" } }; - * const extruded = Notation.create(obj).extrude("car.brand", "carBrand"); - * console.log(obj); - * // { car: { model: "Mustang" } } - * console.log(extruded); - * // { carBrand: "Ford" } - */ - - }, { - key: "extrude", - value: function extrude(notation, newNotation) { - var o = {}; - this.moveTo(o, notation, newNotation); - return o; - } - /** - * Alias for `#extrude` - * @private - * @param {String} notation - - * @param {String} newNotation - - * @returns {Object} - - */ - - }, { - key: "moveToNew", - value: function moveToNew(notation, newNotation) { - return this.extrude(notation, newNotation); - } // -------------------------------- - // STATIC MEMBERS - // -------------------------------- - - /** - * Basically constructs a new `Notation` instance. - * @chainable - * @param {Object|Array} [source={}] - The source collection to be notated. - * @param {Object} [options] - Notation options. - * @param {Boolean} [options.strict=false] - Whether to throw when a - * notation path does not exist on the source. (Note that `.inspectGet()` - * and `.inspectRemove()` methods are exceptions). It's recommended to - * set this to `true` and prevent silent failures if you're working - * with sensitive data. Regardless of `strict` option, it will always - * throw on invalid notation syntax or other crucial failures. - * - * @returns {Notation} - The created instance. - * - * @example - * const obj = { car: { brand: "Dodge", model: "Charger", year: 1970 } }; - * const notation = Notation.create(obj); // equivalent to new Notation(obj) - * notation.get('car.model') // » "Charger" - * notation.remove('car.model').set('car.color', 'red').value - * // » { car: { brand: "Dodge", year: 1970, color: "red" } } - */ - - }, { - key: "options", - get: function get() { - return this._options; - }, - set: function set(value) { - this._options = _objectSpread(_objectSpread(_objectSpread({}, DEFAULT_OPTS), this._options || {}), value || {}); - } - /** - * Gets the value of the source object. - * @type {Object|Array} - * - * @example - * const person = { name: "Onur" }; - * const me = Notation.create(person) - * .set("age", 36) - * .set("car.brand", "Ford") - * .set("car.model", "Mustang") - * .value; - * console.log(me); // { name: "Onur", age: 36, car: { brand: "Ford", model: "Mustang" } } - * console.log(person === me); // true - */ - - }, { - key: "value", - get: function get() { - return this._source; - } - }], [{ - key: "create", - value: function create(source, options) { - if (arguments.length === 0) { - return new Notation({}); - } - - return new Notation(source, options); - } - /** - * Checks whether the given notation string is valid. Note that the star - * (`*`) (which is a valid character, even if irregular) is NOT treated as - * wildcard here. This checks for regular dot-notation, not a glob-notation. - * For glob notation validation, use `Notation.Glob.isValid()` method. Same - * goes for the negation character/prefix (`!`). - * - * @param {String} notation - The notation string to be checked. - * @returns {Boolean} - - * - * @example - * Notation.isValid('prop1.prop2.prop3'); // true - * Notation.isValid('x'); // true - * Notation.isValid('x.arr[0].y'); // true - * Notation.isValid('x["*"]'); // true - * Notation.isValid('x.*'); // false (this would be valid for Notation#filter() only or Notation.Glob class) - * Notation.isValid('@1'); // false (should be "['@1']") - * Notation.isValid(null); // false - */ - - }, { - key: "isValid", - value: function isValid(notation) { - return typeof notation === 'string' && reVALIDATOR.test(notation); - } - /** - * Splits the given notation string into its notes (levels). - * @param {String} notation Notation string to be splitted. - * @returns {Array} - A string array of notes (levels). - * @throws {NotationError} - If given notation is invalid. - */ - - }, { - key: "split", - value: function split(notation) { - if (!Notation.isValid(notation)) { - throw new _notation_error__WEBPACK_IMPORTED_MODULE_1__["NotationError"](ERR.NOTATION + "'".concat(notation, "'")); - } - - return notation.match(reMATCHER); - } - /** - * Joins the given notes into a notation string. - * @param {String} notes Notes (levels) to be joined. - * @returns {String} Joined notation string. - */ - - }, { - key: "join", - value: function join(notes) { - return _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].joinNotes(notes); - } - /** - * Counts the number of notes/levels in the given notation. - * @alias Notation.countLevels - * @param {String} notation - The notation string to be processed. - * @returns {Number} - Number of notes. - * @throws {NotationError} - If given notation is invalid. - */ - - }, { - key: "countNotes", - value: function countNotes(notation) { - return Notation.split(notation).length; - } - /** - * Alias of `Notation.countNotes`. - * @private - * @param {String} notation - - * @returns {Number} - - */ - - }, { - key: "countLevels", - value: function countLevels(notation) { - return Notation.countNotes(notation); - } - /** - * Gets the first (root) note of the notation string. - * @param {String} notation - The notation string to be processed. - * @returns {String} - First note. - * @throws {NotationError} - If given notation is invalid. - * - * @example - * Notation.first('first.prop2.last'); // "first" - */ - - }, { - key: "first", - value: function first(notation) { - return Notation.split(notation)[0]; - } - /** - * Gets the last note of the notation string. - * @param {String} notation - The notation string to be processed. - * @returns {String} - Last note. - * @throws {NotationError} - If given notation is invalid. - * - * @example - * Notation.last('first.prop2.last'); // "last" - */ - - }, { - key: "last", - value: function last(notation) { - var list = Notation.split(notation); - return list[list.length - 1]; - } - /** - * Gets the parent notation (up to but excluding the last note) - * from the notation string. - * @param {String} notation - The notation string to be processed. - * @returns {String} - Parent note if any. Otherwise, `null`. - * @throws {NotationError} - If given notation is invalid. - * - * @example - * Notation.parent('first.prop2.last'); // "first.prop2" - * Notation.parent('single'); // null - */ - - }, { - key: "parent", - value: function parent(notation) { - var last = Notation.last(notation); - return notation.slice(0, -last.length).replace(/\.$/, '') || null; - } - /** - * Iterates through each note/level of the given notation string. - * @alias Notation.eachLevel - * - * @param {String} notation - The notation string to be iterated through. - * @param {Function} callback - The callback function to be invoked on - * each iteration. To break out of the loop, return `false` from within the - * callback. - * Callback signature: `callback(levelNotation, note, index, list) { ... }` - * - * @returns {void} - * @throws {NotationError} - If given notation is invalid. - * - * @example - * const notation = 'first.prop2.last'; - * Notation.eachNote(notation, function (levelNotation, note, index, list) { - * console.log(index, note, levelNotation); - * }); - * // 0 "first" "first" - * // 1 "first.prop2" "prop2" - * // 2 "first.prop2.last" "last" - */ - - }, { - key: "eachNote", - value: function eachNote(notation, callback) { - var notes = Notation.split(notation); - var levelNotes = []; - _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].each(notes, function (note, index) { - levelNotes.push(note); - if (callback(Notation.join(levelNotes), note, index, notes) === false) return false; - }, Notation); - } - /** - * Alias of `Notation.eachNote`. - * @private - * @param {String} notation - - * @param {Function} callback - - * @returns {void} - */ - - }, { - key: "eachLevel", - value: function eachLevel(notation, callback) { - Notation.eachNote(notation, callback); - } - }]); - - return Notation; -}(); -/** - * Error class specific to `Notation`. - * @private - * - * @class - * @see `{@link #Notation.Error}` - */ - - -Notation.Error = _notation_error__WEBPACK_IMPORTED_MODULE_1__["NotationError"]; -/** - * Utility for validating, comparing and sorting dot-notation globs. - * This is internally used by `Notation` class. - * @private - * - * @class - * @see `{@link #Notation.Glob}` - */ - -Notation.Glob = _notation_glob__WEBPACK_IMPORTED_MODULE_0__["Glob"]; -/** - * Undocumented - * @private - */ - -Notation.utils = _utils__WEBPACK_IMPORTED_MODULE_2__["utils"]; // -------------------------------- -// HELPERS -// -------------------------------- - -/** - * Deep iterates through each note (level) of each item in the given - * collection. - * @private - * @param {Object|Array} collection A data object or an array, as the source. - * @param {Function} callback A function to be executed on each iteration, - * with the following arguments: `(levelNotation, note, value, collection)` - * @param {Boolean} [reverseIfArray=false] Set to `true` to iterate with - * `eachRight` to prevent shifted indexes when removing items from arrays. - * @param {Boolean} [byLevel=false] Indicates whether to iterate notations by - * each level or by the end value. For example, if we have a collection of - * `{a: { b: true } }`, and `byLevel` is set; the callback will be invoked on - * the following notations: `a`, `a.b`. Otherwise, it will be invoked only on - * `a.b`. - * @param {String} [parentNotation] Storage for parent (previous) notation. - * @param {Collection} [topSource] Storage for initial/main collection. - * @returns {void} - */ - -function _each(collection, callback) { - var reverseIfArray = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; - var byLevel = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; - var parentNotation = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : null; - var topSource = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : null; - // eslint-disable-line max-params - var source = topSource || collection; // if (!utils.isCollection(collection)) throw ... // no need - - _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].eachItem(collection, function (value, keyOrIndex) { - var note = typeof keyOrIndex === 'number' ? "[".concat(keyOrIndex, "]") : keyOrIndex; - var currentNotation = Notation.join([parentNotation, note]); - var isCollection = _utils__WEBPACK_IMPORTED_MODULE_2__["utils"].isCollection(value); // if it's not a collection we'll execute the callback. if it's a - // collection but byLevel is set, we'll also execute the callback. - - if (!isCollection || byLevel) { - if (callback(currentNotation, note, value, source) === false) return false; - } // deep iterating if collection - - - if (isCollection) _each(value, callback, reverseIfArray, byLevel, currentNotation, source); - }, null, reverseIfArray); -} // -------------------------------- -// EXPORT -// -------------------------------- - - - - -/***/ }), - -/***/ "./src/index.js": -/*!**********************!*\ - !*** ./src/index.js ***! - \**********************/ -/*! exports provided: Notation */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony import */ var _core_notation__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./core/notation */ "./src/core/notation.js"); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Notation", function() { return _core_notation__WEBPACK_IMPORTED_MODULE_0__["Notation"]; }); - -/* istanbul ignore file */ - - -/***/ }), - -/***/ "./src/utils.js": -/*!**********************!*\ - !*** ./src/utils.js ***! - \**********************/ -/*! exports provided: utils */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "utils", function() { return utils; }); -/* harmony import */ var _core_notation_error__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./core/notation.error */ "./src/core/notation.error.js"); - -var objProto = Object.prototype; -var symValueOf = typeof Symbol === 'function' ? Symbol.prototype.valueOf -/* istanbul ignore next */ -: null; // never use 'g' (global) flag in regexps below - -var VAR = /^[a-z$_][a-z$_\d]*$/i; -var ARRAY_NOTE = /^\[(\d+)\]$/; -var ARRAY_GLOB_NOTE = /^\[(\d+|\*)\]$/; -var OBJECT_BRACKETS = /^\[(?:'(.*)'|"(.*)"|`(.*)`)\]$/; -var WILDCARD = /^(\[\*\]|\*)$/; // matches `*` and `[*]` if outside of quotes. - -var WILDCARDS = /(\*|\[\*\])(?=(?:[^"]|"[^"]*")*$)(?=(?:[^']|'[^']*')*$)/; // matches trailing wildcards at the end of a non-negated glob. -// e.g. `x.y.*[*].*` » $1 = `x.y`, $2 = `.*[*].*` - -var NON_NEG_WILDCARD_TRAIL = /^(?!!)(.+?)(\.\*|\[\*\])+$/; -var NEGATE_ALL = /^!(\*|\[\*\])$/; // ending with '.*' or '[*]' - -var _reFlags = /\w*$/; -var utils = { - re: { - VAR: VAR, - ARRAY_NOTE: ARRAY_NOTE, - ARRAY_GLOB_NOTE: ARRAY_GLOB_NOTE, - OBJECT_BRACKETS: OBJECT_BRACKETS, - WILDCARD: WILDCARD, - WILDCARDS: WILDCARDS, - NON_NEG_WILDCARD_TRAIL: NON_NEG_WILDCARD_TRAIL, - NEGATE_ALL: NEGATE_ALL - }, - type: function type(o) { - return objProto.toString.call(o).match(/\s(\w+)/i)[1].toLowerCase(); - }, - isCollection: function isCollection(o) { - var t = utils.type(o); - return t === 'object' || t === 'array'; - }, - isset: function isset(o) { - return o !== undefined && o !== null; - }, - ensureArray: function ensureArray(o) { - if (utils.type(o) === 'array') return o; - return o === null || o === undefined ? [] : [o]; - }, - // simply returning true will get rid of the "holes" in the array. - // e.g. [0, , 1, , undefined, , , 2, , , null].filter(() => true); - // ——» [0, 1, undefined, 2, null] - // cleanSparseArray(a) { - // return a.filter(() => true); - // }, - // added _collectionType for optimization (in loops) - hasOwn: function hasOwn(collection, keyOrIndex, _collectionType) { - if (!collection) return false; - var isArr = (_collectionType || utils.type(collection)) === 'array'; - - if (!isArr && typeof keyOrIndex === 'string') { - return keyOrIndex && objProto.hasOwnProperty.call(collection, keyOrIndex); - } - - if (typeof keyOrIndex === 'number') { - return keyOrIndex >= 0 && keyOrIndex < collection.length; - } - - return false; - }, - cloneDeep: function cloneDeep(collection) { - var t = utils.type(collection); - - switch (t) { - case 'date': - return new Date(collection.valueOf()); - - case 'regexp': - { - var flags = _reFlags.exec(collection).toString(); - - var copy = new collection.constructor(collection.source, flags); - copy.lastIndex = collection.lastIndex; - return copy; - } - - case 'symbol': - return symValueOf ? Object(symValueOf.call(collection)) - /* istanbul ignore next */ - : collection; - - case 'array': - return collection.map(utils.cloneDeep); - - case 'object': - { - var _copy = {}; // only enumerable string keys - - Object.keys(collection).forEach(function (k) { - _copy[k] = utils.cloneDeep(collection[k]); - }); - return _copy; - } - // primitives copied over by value - // case 'string': - // case 'number': - // case 'boolean': - // case 'null': - // case 'undefined': - - default: - // others will be referenced - return collection; - } - }, - // iterates over elements of an array, executing the callback for each - // element. - each: function each(array, callback, thisArg) { - var len = array.length; - var index = -1; - - while (++index < len) { - if (callback.apply(thisArg, [array[index], index, array]) === false) return; - } - }, - eachRight: function eachRight(array, callback, thisArg) { - var index = array.length; - - while (index--) { - if (callback.apply(thisArg, [array[index], index, array]) === false) return; - } - }, - eachProp: function eachProp(object, callback, thisArg) { - var keys = Object.keys(object); - var index = -1; - - while (++index < keys.length) { - var key = keys[index]; - if (callback.apply(thisArg, [object[key], key, object]) === false) return; - } - }, - eachItem: function eachItem(collection, callback, thisArg) { - var reverseIfArray = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; - - if (utils.type(collection) === 'array') { - // important! we should iterate with eachRight to prevent shifted - // indexes when removing items from arrays. - return reverseIfArray ? utils.eachRight(collection, callback, thisArg) : utils.each(collection, callback, thisArg); - } - - return utils.eachProp(collection, callback, thisArg); - }, - pregQuote: function pregQuote(str) { - var re = /[.\\+*?[^\]$(){}=!<>|:-]/g; - return String(str).replace(re, '\\$&'); - }, - stringOrArrayOf: function stringOrArrayOf(o, value) { - return typeof value === 'string' && (o === value || utils.type(o) === 'array' && o.length === 1 && o[0] === value); - }, - hasSingleItemOf: function hasSingleItemOf(arr, itemValue) { - return arr.length === 1 && (arguments.length === 2 ? arr[0] === itemValue : true); - }, - // remove trailing/redundant wildcards if not negated - removeTrailingWildcards: function removeTrailingWildcards(glob) { - // return glob.replace(/(.+?)(\.\*|\[\*\])*$/, '$1'); - return glob.replace(NON_NEG_WILDCARD_TRAIL, '$1'); - }, - normalizeNote: function normalizeNote(note) { - if (VAR.test(note)) return note; // check array index notation e.g. `[1]` - - var m = note.match(ARRAY_NOTE); - if (m) return parseInt(m[1], 10); // check object bracket notation e.g. `["a-b"]` - - m = note.match(OBJECT_BRACKETS); - if (m) return m[1] || m[2] || m[3]; - throw new _core_notation_error__WEBPACK_IMPORTED_MODULE_0__["NotationError"]("Invalid note: '".concat(note, "'")); - }, - joinNotes: function joinNotes(notes) { - var lastIndex = notes.length - 1; - return notes.map(function (current, i) { - if (!current) return ''; - var next = lastIndex >= i + 1 ? notes[i + 1] : null; - var dot = next ? next[0] === '[' ? '' : '.' : ''; - return current + dot; - }).join(''); - }, - getNewNotation: function getNewNotation(newNotation, notation) { - var errMsg = "Invalid new notation: '".concat(newNotation, "'"); // note validations (for newNotation and notation) are already made by - // other methods in the flow. - - var newN; - - if (typeof newNotation === 'string') { - newN = newNotation.trim(); - if (!newN) throw new _core_notation_error__WEBPACK_IMPORTED_MODULE_0__["NotationError"](errMsg); - return newN; - } - - if (notation && !utils.isset(newNotation)) return notation; - throw new _core_notation_error__WEBPACK_IMPORTED_MODULE_0__["NotationError"](errMsg); - } -}; - - -/***/ }) - -/******/ }); -}); -//# sourceMappingURL=notation.js.map \ No newline at end of file diff --git a/lib/notation.js.map b/lib/notation.js.map deleted file mode 100644 index 74533c9..0000000 --- a/lib/notation.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["webpack://notation/webpack/universalModuleDefinition","webpack://notation/webpack/bootstrap","webpack://notation/./src/core/notation.error.js","webpack://notation/./src/core/notation.glob.js","webpack://notation/./src/core/notation.js","webpack://notation/./src/index.js","webpack://notation/./src/utils.js"],"names":["setProto","Object","setPrototypeOf","NotationError","message","prototype","defineProperty","enumerable","writable","value","Error","hasOwnProperty","captureStackTrace","stack","reMATCHER","reVALIDATOR","RegExp","re","utils","ERR_INVALID","Glob","glob","ins","_inspect","notes","split","absGlob","_","parent","undefined","regexp","notation","Notation","isValid","_covers","restrictive","_intersect","isNegated","toRegExp","length","slice","last","replace","test","WILDCARDS","g","indexOf","pregQuote","globA","globB","match","a","b","notesA","notesB","covers","fn","_matchesNote","_coversNote","i","bang","len","Math","max","notesI","push","WILDCARD","joinNotes","trim","removeTrailingWildcards","isArrayGlob","normalize","neg","aIdxCompare","_compareArrayItemGlobs","wildCountA","wildCountB","globList","sort","compare","original","ensureArray","list","concat","_negFirstSort","_negLastSort","map","negateAll","normalized","ignored","intersections","checkAddIntersection","gA","gB","inter","hasInverted","_invert","eachRight","indexA","NEGATE_ALL","duplicate","hasExactNeg","negCoversPos","negCoveredByPos","negCoveredByNeg","posCoversPos","posCoveredByNeg","posCoveredByPos","indexB","JSON","stringify","splice","_isReverseOf","coversB","coveredByB","keepNeg","keepPos","keep","keys","globsListA","globsListB","union","notCovered","hasExact","negCoversNeg","posCoversNeg","globsA","globsB","_compareUnion","listA","listB","bIsArr","ARRAY_GLOB_NOTE","normalizeNote","_idxVal","note","parseInt","_compArrIdx","lastA","lastB","iA","iB","reANote","_rx","negA","negB","ERR","SOURCE","DEST","NOTATION","NOTA_OBJ","NO_INDEX","NO_PROP","DEFAULT_OPTS","freeze","strict","preserveIndices","source","options","arguments","_source","isCollection","_isArray","type","callback","_each","level","eachNote","levelNotation","index","hasOwn","each","cloneDeep","o","key","create","merge","expand","result","has","lastNoteNormalized","lastNote","parentIsArray","parentNotation","get","inspectGet","defaultValue","msg","mode","currentIsLast","nCurrentNote","nNextNote","nextIsArrayNote","insertErrMsg","notationsObject","overwrite","set","notations","inspectRemove","globs","empty","cloned","firstIsWildcard","filtered","shift","globNotation","levels","emptyValue","eType","errGlobIntegrity","insRemove","vType","errMsg","isValSet","isset","setMode","insGet","reverseIterateIfArray","originalNotation","originalIsCovered","levelLen","remove","destination","newNotation","newN","getNewNotation","target","moveTo","rename","copyTo","extract","extrude","_options","countNotes","levelNotes","join","collection","reverseIfArray","byLevel","topSource","eachItem","keyOrIndex","currentNotation","objProto","symValueOf","Symbol","valueOf","VAR","ARRAY_NOTE","OBJECT_BRACKETS","NON_NEG_WILDCARD_TRAIL","_reFlags","toString","call","toLowerCase","t","_collectionType","isArr","Date","flags","exec","copy","constructor","lastIndex","forEach","k","array","thisArg","apply","eachProp","object","str","String","stringOrArrayOf","hasSingleItemOf","arr","itemValue","m","current","next","dot"],"mappings":"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","file":"notation.js","sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine(\"notation\", [], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"notation\"] = factory();\n\telse\n\t\troot[\"notation\"] = factory();\n})(this, function() {\nreturn "," \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"lib/\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = \"./src/index.js\");\n","/* eslint consistent-this:0, no-prototype-builtins:0 */\n\nconst setProto = Object.setPrototypeOf;\n\n/**\n * Error class specific to `Notation`.\n * @class\n * @name Notation.Error\n */\nclass NotationError extends Error {\n\n /**\n * Initializes a new `Notation.Error` instance.\n * @hideconstructor\n * @constructs Notation.Error\n * @param {String} message - The error message.\n */\n constructor(message = '') {\n super(message);\n setProto(this, NotationError.prototype);\n\n Object.defineProperty(this, 'name', {\n enumerable: false,\n writable: false,\n value: 'NotationError'\n });\n\n Object.defineProperty(this, 'message', {\n enumerable: false,\n writable: true,\n value: message\n });\n\n /* istanbul ignore else */\n if (Error.hasOwnProperty('captureStackTrace')) { // V8\n Error.captureStackTrace(this, NotationError);\n } else {\n Object.defineProperty(this, 'stack', {\n enumerable: false,\n writable: false,\n value: (new Error(message)).stack\n });\n }\n }\n\n}\n\nexport { NotationError };\n","/* eslint no-use-before-define:0, consistent-return:0, max-statements:0 */\n\nimport { Notation } from './notation';\nimport { NotationError } from './notation.error';\nimport { utils } from '../utils';\n\n// http://www.linfo.org/wildcard.html\n// http://en.wikipedia.org/wiki/Glob_%28programming%29\n// http://en.wikipedia.org/wiki/Wildcard_character#Computing\n\n// created test @ https://regex101.com/r/U08luj/2\nconst reMATCHER = /(\\[(\\d+|\\*|\".*\"|'.*')\\]|[a-z$_][a-z$_\\d]*|\\*)/gi; // ! negation should be removed first\n// created test @ https://regex101.com/r/mC8unE/3\n// /^!?(\\*|[a-z$_][a-z$_\\d]*|\\[(\\d+|\".*\"|'.*'|`.*`|\\*)\\])(\\[(\\d+|\".*\"|'.*'|`.*`|\\*)\\]|\\.[a-z$_][a-z$_\\d]*|\\.\\*)*$/i\nconst reVALIDATOR = new RegExp(\n '^'\n + '!?(' // optional negation, only in the front\n + '\\\\*' // wildcard star\n + '|' // OR\n + '[a-z$_][a-z$_\\\\d]*' // JS variable syntax\n + '|' // OR\n + '\\\\[(\\\\d+|\\\\*|\".*\"|\\'.*\\')\\\\]' // array index or wildcard, or object bracket notation\n + ')' // exactly once\n + '('\n + '\\\\[(\\\\d+|\\\\*|\".*\"|\\'.*\\')\\\\]' // followed by same\n + '|' // OR\n + '\\\\.[a-z$_][a-z$_\\\\d]*' // dot, then JS variable syntax\n + '|' // OR\n + '\\\\.\\\\*' // dot, then wildcard star\n + ')*' // (both) may repeat any number of times\n + '$'\n , 'i'\n);\n\nconst { re } = utils;\nconst ERR_INVALID = 'Invalid glob notation: ';\n\n/**\n * `Notation.Glob` is a utility for validating, comparing and sorting\n * dot-notation globs.\n *\n * You can use {@link http://www.linfo.org/wildcard.html|wildcard} stars `*`\n * and negate the notation by prepending a bang `!`. A star will include all\n * the properties at that level and a negated notation will be excluded.\n * @name Glob\n * @memberof Notation\n * @class\n *\n * @example\n * // for the following object;\n * { name: 'John', billing: { account: { id: 1, active: true } } };\n *\n * 'billing.account.*' // represents value `{ id: 1, active: true }`\n * 'billing.account.id' // represents value `1`\n * '!billing.account.*' // represents value `{ name: 'John' }`\n * 'name' // represents `'John'`\n * '*' // represents the whole object\n *\n * @example\n * var glob = new Notation.Glob('billing.account.*');\n * glob.test('billing.account.id'); // true\n */\nclass Glob {\n\n /**\n * Constructs a `Notation.Glob` object with the given glob string.\n * @constructs Notation.Glob\n * @param {String} glob - Notation string with globs.\n *\n * @throws {NotationError} - If given notation glob is invalid.\n */\n constructor(glob) {\n const ins = Glob._inspect(glob);\n const notes = Glob.split(ins.absGlob);\n this._ = {\n ...ins,\n notes,\n // below props will be set at first getter call\n parent: undefined, // don't set to null\n regexp: undefined\n };\n }\n\n // --------------------------------\n // INSTANCE PROPERTIES\n // --------------------------------\n\n /**\n * Gets the normalized glob notation string.\n * @name Notation.Glob#glob\n * @type {String}\n */\n get glob() {\n return this._.glob;\n }\n\n /**\n * Gets the absolute glob notation without the negation prefix `!` and\n * redundant trailing wildcards.\n * @name Notation.Glob#absGlob\n * @type {String}\n */\n get absGlob() {\n return this._.absGlob;\n }\n\n /**\n * Specifies whether this glob is negated with a `!` prefix.\n * @name Notation.Glob#isNegated\n * @type {Boolean}\n */\n get isNegated() {\n return this._.isNegated;\n }\n\n /**\n * Represents this glob in regular expressions.\n * Note that the negation prefix (`!`) is ignored, if any.\n * @name Notation.Glob#regexp\n * @type {RegExp}\n */\n get regexp() {\n // setting on first call instead of in constructor, for performance\n // optimization.\n this._.regexp = this._.regexp || Glob.toRegExp(this.absGlob);\n return this._.regexp;\n }\n\n /**\n * List of notes (levels) of this glob notation. Note that trailing,\n * redundant wildcards are removed from the original glob notation.\n * @name Notation.Glob#notes\n * @alias Notation.Glob#levels\n * @type {Array}\n */\n get notes() {\n return this._.notes;\n }\n\n /**\n * Alias of `Notation.Glob#notes`.\n * @private\n * @name Notation.Glob#notes\n * @alias Notation.Glob#levels\n * @type {Array}\n */\n get levels() {\n return this._.notes;\n }\n\n /**\n * Gets the first note of this glob notation.\n * @name Notation.Glob#first\n * @type {String}\n */\n get first() {\n return this.notes[0];\n }\n\n /**\n * Gets the last note of this glob notation.\n * @name Notation.Glob#last\n * @type {String}\n */\n get last() {\n return this.notes[this.notes.length - 1];\n }\n\n /**\n * Gets the parent notation (up to but excluding the last note) from the\n * glob notation string. Note that initially, trailing/redundant wildcards\n * are removed.\n * @name Notation.Glob#parent\n * @type {String}\n *\n * @example\n * const glob = Notation.Glob.create;\n * glob('first.second.*').parent; // \"first.second\"\n * glob('*.x.*').parent; // \"*\" (\"*.x.*\" normalizes to \"*.x\")\n * glob('*').parent; // null (no parent)\n */\n get parent() {\n // setting on first call instead of in constructor, for performance\n // optimization.\n if (this._.parent === undefined) {\n this._.parent = this.notes.length > 1\n ? this.absGlob.slice(0, -this.last.length).replace(/\\.$/, '')\n : null;\n }\n return this._.parent;\n }\n\n // --------------------------------\n // INSTANCE METHODS\n // --------------------------------\n\n /**\n * Checks whether the given notation value matches the source notation\n * glob.\n * @name Notation.Glob#test\n * @function\n * @param {String} notation - The notation string to be tested. Cannot have\n * any globs.\n * @returns {Boolean} -\n * @throws {NotationError} - If given `notation` is not valid or contains\n * any globs.\n *\n * @example\n * const glob = new Notation.Glob('!prop.*.name');\n * glob.test(\"prop.account.name\"); // true\n */\n test(notation) {\n if (!Notation.isValid(notation)) {\n throw new NotationError(`Invalid notation: '${notation}'`);\n }\n // return this.regexp.test(notation);\n return Glob._covers(this, notation);\n }\n\n /**\n * Specifies whether this glob notation can represent (or cover) the given\n * glob notation. Note that negation prefix is ignored, if any.\n * @name Notation.Glob#covers\n * @function\n *\n * @param {String|Array|Glob} glob Glob notation string, glob\n * notes array or a `Notation.Glob` instance.\n * @returns {Boolean} -\n *\n * @example\n * const glob = Notation.Glob.create;\n * glob('*.y').covers('x.y') // true\n * glob('x[*].y').covers('x[*]') // false\n */\n covers(glob) {\n return Glob._covers(this, glob);\n }\n\n /**\n * Gets the intersection of this and the given glob notations. When\n * restrictive, if any one of them is negated, the outcome is negated.\n * Otherwise, only if both of them are negated, the outcome is negated.\n * @name Notation.Glob#intersect\n * @function\n *\n * @param {String} glob - Second glob to be used.\n * @param {Boolean} [restrictive=false] - Whether the intersection should\n * be negated when one of the globs is negated.\n * @returns {String} - Intersection notation if any; otherwise `null`.\n *\n * @example\n * const glob = Notation.Glob.create;\n * glob('x.*').intersect('!*.y') // 'x.y'\n * glob('x.*').intersect('!*.y', true) // '!x.y'\n */\n intersect(glob, restrictive = false) {\n return Glob._intersect(this.glob, glob, restrictive);\n }\n\n // --------------------------------\n // STATIC MEMBERS\n // --------------------------------\n\n /**\n * Basically constructs a new `Notation.Glob` instance\n * with the given glob string.\n * @name Notation.Glob.create\n * @function\n *\n * @param {String} glob - The source notation glob.\n * @returns {Glob} -\n *\n * @example\n * const glob = Notation.Glob.create(strGlob);\n * // equivalent to:\n * const glob = new Notation.Glob(strGlob);\n */\n static create(glob) {\n return new Glob(glob);\n }\n\n // Created test at: https://regex101.com/r/tJ7yI9/4\n /**\n * Validates the given notation glob.\n * @name Notation.Glob.isValid\n * @function\n *\n * @param {String} glob - Notation glob to be validated.\n * @returns {Boolean} -\n */\n static isValid(glob) {\n return typeof glob === 'string' && reVALIDATOR.test(glob);\n }\n\n /**\n * Specifies whether the given glob notation includes any valid wildcards\n * (`*`) or negation bang prefix (`!`).\n * @name Notation.Glob.hasMagic\n * @function\n *\n * @param {String} glob - Glob notation to be checked.\n * @returns {Boolean} -\n */\n static hasMagic(glob) {\n return Glob.isValid(glob) && (re.WILDCARDS.test(glob) || glob[0] === '!');\n }\n\n /**\n * Gets a regular expressions instance from the given glob notation.\n * Note that the bang `!` prefix will be ignored if the given glob is negated.\n * @name Notation.Glob.toRegExp\n * @function\n *\n * @param {String} glob - Glob notation to be converted.\n *\n * @returns {RegExp} - A `RegExp` instance from the glob.\n *\n * @throws {NotationError} - If given notation glob is invalid.\n */\n static toRegExp(glob) {\n if (!Glob.isValid(glob)) {\n throw new NotationError(`${ERR_INVALID} '${glob}'`);\n }\n\n let g = glob.indexOf('!') === 0 ? glob.slice(1) : glob;\n g = utils.pregQuote(g)\n // `[*]` always represents array index e.g. `[1]`. so we'd replace\n // `\\[\\*\\]` with `\\[\\d+\\]` but we should also watch for quotes: e.g.\n // `[\"x[*]y\"]`\n .replace(/\\\\\\[\\\\\\*\\\\\\](?=(?:[^\"]|\"[^\"]*\")*$)(?=(?:[^']|'[^']*')*$)/g, '\\\\[\\\\d+\\\\]')\n // `*` within quotes (e.g. ['*']) is non-wildcard, just a regular star char.\n // `*` outside of quotes is always JS variable syntax e.g. `prop.*`\n .replace(/\\\\\\*(?=(?:[^\"]|\"[^\"]*\")*$)(?=(?:[^']|'[^']*')*$)/g, '[a-z$_][a-z$_\\\\d]*')\n .replace(/\\\\\\?/g, '.');\n return new RegExp('^' + g + '(?:[\\\\[\\\\.].+|$)', 'i');\n // it should either end ($) or continue with a dot or bracket. So for\n // example, `company.*` will produce `/^company\\.[a-z$_][a-z$_\\\\d]*(?:[\\\\[|\\\\.].+|$)/`\n // which will match both `company.name` and `company.address.street` but\n // will not match `some.company.name`. Also `!password` will not match\n // `!password_reset`.\n }\n\n /**\n * Specifies whether first glob notation can represent (or cover) the\n * second.\n * @name Notation.Glob._covers\n * @function\n * @private\n *\n * @param {String|Object|Glob} globA Source glob notation string\n * or inspection result object or `Notation.Glob` instance.\n * @param {String|Object|Glob} globB Glob notation string or\n * inspection result object or `Notation.Glob` instance.\n * @param {Boolean} [match=false] Check whether notes match instead of\n * `globA` covers `globB`.\n * @returns {Boolean} -\n *\n * @example\n * const { covers } = Notation.Glob;\n * covers('*.y', 'x.y') // true\n * covers('x.y', '*.y') // false\n * covers('x.y', '*.y', true) // true\n * covers('x[*].y', 'x[*]') // false\n */\n static _covers(globA, globB, match = false) {\n const a = typeof globA === 'string'\n ? new Glob(globA)\n : globA; // assume (globA instanceof Notation.Glob || utils.type(globA) === 'object')\n\n const b = typeof globB === 'string'\n ? new Glob(globB)\n : globB;\n\n const notesA = a.notes || Glob.split(a.absGlob);\n const notesB = b.notes || Glob.split(b.absGlob);\n\n if (!match) {\n // !x.*.* does not cover !x.* or x.* bec. !x.*.* ≠ x.* ≠ x\n // x.*.* covers x.* bec. x.*.* = x.* = x\n if (a.isNegated && notesA.length > notesB.length) return false;\n }\n\n let covers = true;\n const fn = match ? _matchesNote : _coversNote;\n for (let i = 0; i < notesA.length; i++) {\n if (!fn(notesA[i], notesB[i])) {\n covers = false;\n break;\n }\n }\n return covers;\n }\n\n /**\n * Gets the intersection notation of two glob notations. When restrictive,\n * if any one of them is negated, the outcome is negated. Otherwise, only\n * if both of them are negated, the outcome is negated.\n * @name Notation.Glob._intersect\n * @function\n * @private\n *\n * @param {String} globA - First glob to be used.\n * @param {String} globB - Second glob to be used.\n * @param {Boolean} [restrictive=false] - Whether the intersection should\n * be negated when one of the globs is negated.\n * @returns {String} - Intersection notation if any; otherwise `null`.\n * @example\n * _intersect('!*.y', 'x.*', false) // 'x.y'\n * _intersect('!*.y', 'x.*', true) // '!x.y'\n */\n static _intersect(globA, globB, restrictive = false) {\n // const bang = restrictive\n // ? (globA[0] === '!' || globB[0] === '!' ? '!' : '')\n // : (globA[0] === '!' && globB[0] === '!' ? '!' : '');\n\n const notesA = Glob.split(globA, true);\n const notesB = Glob.split(globB, true);\n\n let bang;\n if (restrictive) {\n bang = globA[0] === '!' || globB[0] === '!' ? '!' : '';\n } else {\n if (globA[0] === '!' && globB[0] === '!') {\n bang = '!';\n } else {\n bang = ((notesA.length > notesB.length && globA[0] === '!')\n || (notesB.length > notesA.length && globB[0] === '!'))\n ? '!'\n : '';\n }\n }\n\n const len = Math.max(notesA.length, notesB.length);\n let notesI = [];\n let a, b;\n // x.* ∩ *.y » x.y\n // x.*.* ∩ *.y » x.y.*\n // x.*.z ∩ *.y » x.y.z\n // x.y ∩ *.b » (n/a)\n // x.y ∩ a.* » (n/a)\n for (let i = 0; i < len; i++) {\n a = notesA[i];\n b = notesB[i];\n if (a === b) {\n notesI.push(a);\n } else if (a && re.WILDCARD.test(a)) {\n if (!b) {\n notesI.push(a);\n } else {\n notesI.push(b);\n }\n } else if (b && re.WILDCARD.test(b)) {\n if (!a) {\n notesI.push(b);\n } else {\n notesI.push(a);\n }\n } else if (a && !b) {\n notesI.push(a);\n } else if (!a && b) {\n notesI.push(b);\n } else { // if (a !== b) {\n notesI = [];\n break;\n }\n }\n\n if (notesI.length > 0) return bang + utils.joinNotes(notesI);\n return null;\n }\n\n /**\n * Undocumented.\n * @name Notation.Glob._inspect\n * @function\n * @private\n *\n * @param {String} glob -\n * @returns {Object} -\n */\n static _inspect(glob) {\n let g = glob.trim();\n if (!Glob.isValid(g)) {\n throw new NotationError(`${ERR_INVALID} '${glob}'`);\n }\n const isNegated = g[0] === '!';\n // trailing wildcards are only redundant if not negated\n if (!isNegated) g = utils.removeTrailingWildcards(g);\n const absGlob = isNegated ? g.slice(1) : g;\n return {\n glob: g,\n absGlob,\n isNegated,\n // e.g. [*] or [1] are array globs. [\"1\"] is not.\n isArrayGlob: (/^\\[[^'\"]/).test(absGlob)\n };\n }\n\n /**\n * Splits the given glob notation string into its notes (levels). Note that\n * this will exclude the `!` negation prefix, if it exists.\n * @name Notation.Glob.split\n * @function\n *\n * @param {String} glob Glob notation string to be splitted.\n * @param {String} [normalize=false] Whether to remove trailing, redundant\n * wildcards.\n * @returns {Array} - A string array of glob notes (levels).\n * @throws {NotationError} - If given glob notation is invalid.\n *\n * @example\n * Notation.Glob.split('*.list[2].prop') // ['*', 'list', '[2]', 'prop']\n * // you can get the same result from the .notes property of a Notation.Glob instance.\n */\n static split(glob, normalize = false) {\n if (!Glob.isValid(glob)) {\n throw new NotationError(`${ERR_INVALID} '${glob}'`);\n }\n const neg = glob[0] === '!';\n // trailing wildcards are redundant only when not negated\n const g = !neg && normalize ? utils.removeTrailingWildcards(glob) : glob;\n return g.replace(/^!/, '').match(reMATCHER);\n }\n\n /**\n * Compares two given notation globs and returns an integer value as a\n * result. This is generally used to sort glob arrays. Loose globs (with\n * stars especially closer to beginning of the glob string) and globs\n * representing the parent/root of the compared property glob come first.\n * Verbose/detailed/exact globs come last. (`* < *.abc < abc`).\n *\n * For instance; `store.address` comes before `store.address.street`. So\n * this works both for `*, store.address.street, !store.address` and `*,\n * store.address, !store.address.street`. For cases such as `prop.id` vs\n * `!prop.id` which represent the same property; the negated glob comes\n * last.\n * @name Notation.Glob.compare\n * @function\n *\n * @param {String} globA - First notation glob to be compared.\n * @param {String} globB - Second notation glob to be compared.\n *\n * @returns {Number} - Returns `-1` if `globA` comes first, `1` if `globB`\n * comes first and `0` if equivalent priority.\n *\n * @throws {NotationError} - If either `globA` or `globB` is invalid glob\n * notation.\n *\n * @example\n * const { compare } = Notation.Glob;\n * compare('*', 'info.user') // -1\n * compare('*', '[*]') // 0\n * compare('info.*.name', 'info.user') // 1\n */\n static compare(globA, globB) {\n // trivial case, both are exactly the same!\n // or both are wildcard e.g. `*` or `[*]`\n if (globA === globB || (re.WILDCARD.test(globA) && re.WILDCARD.test(globB))) return 0;\n\n const a = new Glob(globA);\n const b = new Glob(globB);\n\n // Check depth (number of levels)\n if (a.notes.length === b.notes.length) {\n // check and compare if these are globs that represent items in the\n // \"same\" array. if not, this will return 0.\n const aIdxCompare = _compareArrayItemGlobs(a, b);\n // we'll only continue comparing if 0 is returned\n if (aIdxCompare !== 0) return aIdxCompare;\n\n // count wildcards\n const wildCountA = (a.absGlob.match(re.WILDCARDS) || []).length;\n const wildCountB = (b.absGlob.match(re.WILDCARDS) || []).length;\n if (wildCountA === wildCountB) {\n // check for negation\n if (!a.isNegated && b.isNegated) return -1;\n if (a.isNegated && !b.isNegated) return 1;\n // both are negated or neither are, return alphabetical\n return a.absGlob < b.absGlob ? -1 : (a.absGlob > b.absGlob ? 1 : 0);\n }\n return wildCountA > wildCountB ? -1 : 1;\n }\n\n return a.notes.length < b.notes.length ? -1 : 1;\n }\n\n /**\n * Sorts the notation globs in the given array by their priorities. Loose\n * globs (with stars especially closer to beginning of the glob string);\n * globs representing the parent/root of the compared property glob come\n * first. Verbose/detailed/exact globs come last. (`* < *.y < x.y`).\n *\n * For instance; `store.address` comes before `store.address.street`. For\n * cases such as `prop.id` vs `!prop.id` which represent the same property;\n * the negated glob wins (comes last).\n * @name Notation.Glob.sort\n * @function\n *\n * @param {Array} globList - The notation globs array to be sorted. The\n * passed array reference is modified.\n * @returns {Array} - Logically sorted globs array.\n *\n * @example\n * Notation.Glob.sort(['!prop.*.name', 'prop.*', 'prop.id']) // ['prop.*', 'prop.id', '!prop.*.name'];\n */\n static sort(globList) {\n return globList.sort(Glob.compare);\n }\n\n /**\n * Normalizes the given notation globs array by removing duplicate or\n * redundant items, eliminating extra verbosity (also with intersection\n * globs) and returns a priority-sorted globs array.\n *\n *
    \n *
  • If any exact duplicates found, all except first is removed.\n *
    example: `['car', 'dog', 'car']` normalizes to `['car', 'dog']`.
  • \n *
  • If both normal and negated versions of a glob are found, negated wins.\n *
    example: `['*', 'id', '!id']` normalizes to `['*', '!id']`.
  • \n *
  • If a glob is covered by another, it's removed.\n *
    example: `['car.*', 'car.model']` normalizes to `['car']`.
  • \n *
  • If a negated glob is covered by another glob, it's kept.\n *
    example: `['*', 'car', '!car.model']` normalizes as is.
  • \n *
  • If a negated glob is not covered by another or it does not cover any other;\n * then we check for for intersection glob. If found, adds them to list;\n * removes the original negated.\n *
    example: `['car.*', '!*.model']` normalizes as to `['car', '!car.model']`.
  • \n *
  • In restrictive mode; if a glob is covered by another negated glob, it's removed.\n * Otherwise, it's kept.\n *
    example: `['*', '!car.*', 'car.model']` normalizes to `['*', '!car']` if restrictive.
  • \n *
\n * @name Notation.Glob.normalize\n * @function\n *\n * @param {Array} globList - Notation globs array to be normalized.\n * @param {Boolean} [restrictive=false] - Whether negated items strictly\n * remove every match. Note that, regardless of this option, if any item has an\n * exact negated version; non-negated is always removed.\n * @returns {Array} -\n *\n * @throws {NotationError} - If any item in globs list is invalid.\n *\n * @example\n * const { normalize } = Notation.Glob;\n * normalize(['*', '!id', 'name', '!car.model', 'car.*', 'id', 'name']); // ['*', '!id', '!car.model']\n * normalize(['!*.id', 'user.*', 'company']); // ['company', 'user', '!company.id', '!user.id']\n * normalize(['*', 'car.model', '!car.*']); // [\"*\", \"!car.*\", \"car.model\"]\n * // restrictive normalize:\n * normalize(['*', 'car.model', '!car.*'], true); // [\"*\", \"!car.*\"]\n */\n static normalize(globList, restrictive = false) {\n const { _inspect, _covers, _intersect } = Glob;\n\n const original = utils.ensureArray(globList);\n if (original.length === 0) return [];\n\n const list = original\n // prevent mutation\n .concat()\n // move negated globs to top so that we inspect non-negated globs\n // against others first. when complete, we'll sort with our\n // .compare() function.\n .sort(restrictive ? _negFirstSort : _negLastSort)\n // turning string array into inspect-obj array, so that we'll not\n // run _inspect multiple times in the inner loop. this also\n // pre-validates each glob.\n .map(_inspect);\n\n // early return if we have a single item\n if (list.length === 1) {\n const g = list[0];\n // single negated item is redundant\n if (g.isNegated) return [];\n // return normalized\n return [g.glob];\n }\n\n // flag to return an empty array (in restrictive mode), if true.\n let negateAll = false;\n\n // we'll push keepers in this array\n let normalized = [];\n // we'll need to remember excluded globs, so that we can move to next\n // item early.\n const ignored = {};\n\n // storage to keep intersections.\n // using an object to prevent duplicates.\n let intersections = {};\n\n function checkAddIntersection(gA, gB) {\n const inter = _intersect(gA, gB, restrictive);\n if (!inter) return;\n // if the intersection result has an inverted version in the\n // original list, don't add this.\n const hasInverted = restrictive ? false : original.indexOf(_invert(inter)) >= 0;\n // also if intersection result is in the current list, don't add it.\n if (list.indexOf(inter) >= 0 || hasInverted) return;\n intersections[inter] = inter;\n }\n\n // iterate each glob by comparing it to remaining globs.\n utils.eachRight(list, (a, indexA) => {\n\n // if `strict` is enabled, return empty if a negate-all is found\n // (which itself is also redundant if single): '!*' or '![*]'\n if (re.NEGATE_ALL.test(a.glob)) {\n negateAll = true;\n if (restrictive) return false;\n }\n\n // flags\n let duplicate = false;\n let hasExactNeg = false;\n // flags for negated\n let negCoversPos = false;\n let negCoveredByPos = false;\n let negCoveredByNeg = false;\n // flags for non-negated (positive)\n let posCoversPos = false;\n let posCoveredByNeg = false;\n let posCoveredByPos = false;\n\n utils.eachRight(list, (b, indexB) => {\n // don't inspect glob with itself\n if (indexA === indexB) return; // move to next\n // console.log(indexA, a.glob, 'vs', b.glob);\n\n if (a.isArrayGlob !== b.isArrayGlob) {\n throw new NotationError(`Integrity failed. Cannot have both object and array notations for root level: ${JSON.stringify(original)}`);\n }\n\n // remove if duplicate\n if (a.glob === b.glob) {\n list.splice(indexA, 1);\n duplicate = true;\n return false; // break out\n }\n\n // remove if positive has an exact negated (negated wins when\n // normalized) e.g. ['*', 'a', '!a'] => ['*', '!a']\n if (!a.isNegated && _isReverseOf(a, b)) {\n // list.splice(indexA, 1);\n ignored[a.glob] = true;\n hasExactNeg = true;\n return false; // break out\n }\n\n // if already excluded b, go on to next\n if (ignored[b.glob]) return; // next\n\n const coversB = _covers(a, b);\n const coveredByB = coversB ? false : _covers(b, a);\n if (a.isNegated) {\n if (b.isNegated) {\n // if negated (a) covered by any other negated (b); remove (a)!\n if (coveredByB) {\n negCoveredByNeg = true;\n // list.splice(indexA, 1);\n ignored[a.glob] = true;\n return false; // break out\n }\n } else {\n /* istanbul ignore if */\n if (coversB) negCoversPos = true;\n if (coveredByB) negCoveredByPos = true;\n // try intersection if none covers the other and only\n // one of them is negated.\n if (!coversB && !coveredByB) {\n checkAddIntersection(a.glob, b.glob);\n }\n }\n } else {\n if (b.isNegated) {\n // if positive (a) covered by any negated (b); remove (a)!\n if (coveredByB) {\n posCoveredByNeg = true;\n if (restrictive) {\n // list.splice(indexA, 1);\n ignored[a.glob] = true;\n return false; // break out\n }\n return; // next\n }\n // try intersection if none covers the other and only\n // one of them is negated.\n if (!coversB && !coveredByB) {\n checkAddIntersection(a.glob, b.glob);\n }\n } else {\n if (coversB) posCoversPos = coversB;\n // if positive (a) covered by any other positive (b); remove (a)!\n if (coveredByB) {\n posCoveredByPos = true;\n if (restrictive) {\n // list.splice(indexA, 1);\n return false; // break out\n }\n }\n }\n }\n\n });\n\n // const keepNeg = (negCoversPos || negCoveredByPos) && !negCoveredByNeg;\n const keepNeg = restrictive\n ? (negCoversPos || negCoveredByPos) && negCoveredByNeg === false\n : negCoveredByPos && negCoveredByNeg === false;\n const keepPos = restrictive\n ? (posCoversPos || posCoveredByPos === false) && posCoveredByNeg === false\n : posCoveredByNeg || posCoveredByPos === false;\n const keep = duplicate === false\n && hasExactNeg === false\n && (a.isNegated ? keepNeg : keepPos);\n\n if (keep) {\n normalized.push(a.glob);\n } else {\n // this is excluded from final (normalized) list, so mark as\n // ignored (don't remove from \"list\" for now)\n ignored[a.glob] = true;\n }\n });\n\n if (restrictive && negateAll) return [];\n\n intersections = Object.keys(intersections);\n if (intersections.length > 0) {\n // merge normalized list with intersections if any\n normalized = normalized.concat(intersections);\n // we have new (intersection) items, so re-normalize\n return Glob.normalize(normalized, restrictive);\n }\n\n return Glob.sort(normalized);\n }\n\n /**\n * Undocumented. See `.union()`\n * @name Notation.Glob._compareUnion\n * @function\n * @private\n *\n * @param {Array} globsListA -\n * @param {Array} globsListB -\n * @param {Boolean} restrictive -\n * @param {Array} union -\n * @returns {Array} -\n */\n static _compareUnion(globsListA, globsListB, restrictive, union = []) {\n const { _covers } = Glob;\n\n const { _inspect, _intersect } = Glob;\n\n utils.eachRight(globsListA, globA => {\n if (union.indexOf(globA) >= 0) return; // next\n\n const a = _inspect(globA);\n\n // if wildcard only, add...\n if (re.WILDCARD.test(a.absGlob)) {\n union.push(a.glob); // push normalized glob\n return; // next\n }\n\n let notCovered = false;\n let hasExact = false;\n let negCoversNeg = false;\n let posCoversNeg = false;\n let posCoversPos = false;\n let negCoversPos = false;\n\n const intersections = [];\n\n utils.eachRight(globsListB, globB => {\n\n // keep if has exact in the other\n if (globA === globB) hasExact = true;\n\n const b = _inspect(globB);\n\n // keep negated if:\n // 1) any negated covers it\n // 2) no positive covers it\n // keep positive if:\n // 1) no positive covers it OR any negated covers it\n\n notCovered = !_covers(b, a);\n if (notCovered) {\n if (a.isNegated && b.isNegated) {\n const inter = _intersect(a.glob, b.glob, restrictive);\n if (inter && union.indexOf(inter) === -1) intersections.push(inter);\n }\n return; // next\n }\n\n if (a.isNegated) {\n if (b.isNegated) {\n negCoversNeg = !hasExact;\n } else {\n posCoversNeg = true; // set flag\n }\n } else {\n if (!b.isNegated) {\n posCoversPos = !hasExact;\n } else {\n negCoversPos = true; // set flag\n }\n }\n\n });\n\n\n const keep = a.isNegated\n ? (!posCoversNeg || negCoversNeg)\n : (!posCoversPos || negCoversPos);\n\n if (hasExact || keep || (notCovered && !a.isNegated)) {\n union.push(a.glob); // push normalized glob\n return;\n }\n\n if (a.isNegated && posCoversNeg && !negCoversNeg && intersections.length > 0) {\n union = union.concat(intersections);\n }\n\n });\n\n return union;\n }\n\n /**\n * Gets the union from the given couple of glob arrays and returns a new\n * array of globs.\n *
    \n *
  • If the exact same element is found in both\n * arrays, one of them is removed to prevent duplicates.\n *
    example: `['!id', 'name'] ∪ ['!id']` unites to `['!id', 'name']`
  • \n *
  • If any non-negated item is covered by a glob in the same\n * or other array, the redundant item is removed.\n *
    example: `['*', 'name'] ∪ ['email']` unites to `['*']`
  • \n *
  • If one of the arrays contains a negated equivalent of an\n * item in the other array, the negated item is removed.\n *
    example: `['!id'] ∪ ['id']` unites to `['id']`
  • \n *
  • If any item covers/matches a negated item in the other array,\n * the negated item is removed.\n *
    example #1: `['!user.id'] ∪ ['user.*']` unites to `['user']`\n *
    example #2: `['*'] ∪ ['!password']` unites to `['*']`\n *
  • \n *
\n * @name Notation.Glob.union\n * @function\n *\n * @param {Array} globsA - First array of glob strings.\n * @param {Array} globsB - Second array of glob strings.\n * @param {Boolean} [restrictive=false] - Whether negated items in each of\n * the lists, strictly remove every match in themselves (not the cross\n * list). This option is used when pre-normalizing each glob list and\n * normalizing the final union list.\n *\n * @returns {Array} -\n *\n * @example\n * const a = ['user.*', '!user.email', 'car.model', '!*.id'];\n * const b = ['!*.date', 'user.email', 'car', '*.age'];\n * const { union } = Notation.Glob;\n * union(a, b) // ['car', 'user', '*.age', '!car.date', '!user.id']\n */\n static union(globsA, globsB, restrictive) {\n const { normalize, _compareUnion } = Glob;\n\n const listA = normalize(globsA, restrictive);\n const listB = normalize(globsB, restrictive);\n\n if (listA.length === 0) return listB;\n if (listB.length === 0) return listA;\n\n // TODO: below should be optimized\n let union = _compareUnion(listA, listB, restrictive);\n union = _compareUnion(listB, listA, restrictive, union);\n return normalize(union, restrictive);\n }\n\n}\n\n// --------------------------------\n// HELPERS\n// --------------------------------\n\n// used by static _covers\nfunction _coversNote(a, b) {\n if (!a || !b) return false; // glob e.g.: [2] does not cover [2][1]\n const bIsArr = re.ARRAY_GLOB_NOTE.test(b);\n // obj-wildcard a will cover b if not array\n if (a === '*') return !bIsArr;\n // arr-wildcard a will cover b if array\n if (a === '[*]') return bIsArr;\n // seems, a is not wildcard so,\n // if b is wildcard (obj or arr) won't be covered\n if (re.WILDCARD.test(b)) return false;\n // normalize both and check for equality\n // e.g. x.y and x['y'] are the same\n return utils.normalizeNote(a) === utils.normalizeNote(b);\n}\n// function _coversNote(a, b) {\n// if (!a || !b) return false; // glob e.g.: [2] does not cover [2][1]\n// a = utils.normalizeNote(a, true);\n// b = utils.normalizeNote(b, true);\n// if (a === b) return true;\n// const bIsArr = re.ARRAY_GLOB_NOTE.test(b);\n// return (a === '*' && !bIsArr) || (a === '[*]' && bIsArr);\n// }\n// used by static _covers\nfunction _matchesNote(a, b) {\n if (!a || !b) return true; // glob e.g.: [2][1] matches [2] and vice-versa.\n return _coversNote(a, b) || _coversNote(b, a);\n}\n\n// used by _compareArrayItemGlobs() for getting a numeric index from array note.\n// we'll use these indexes to sort higher to lower, as removing order; to\n// prevent shifted indexes.\nfunction _idxVal(note) {\n // we return -1 for wildcard bec. we need it to come last\n\n // below will never execute when called from _compareArrayItemGlobs\n /* istanbul ignore next */\n // if (note === '[*]') return -1;\n\n // e.g. '[2]' » 2\n return parseInt(note.replace(/[[\\]]/, ''), 10);\n}\n\nfunction _compArrIdx(lastA, lastB) {\n const iA = _idxVal(lastA);\n const iB = _idxVal(lastB);\n\n // below will never execute when called from _compareArrayItemGlobs\n /* istanbul ignore next */\n // if (iA === iB) return 0;\n\n return iA > iB ? -1 : 1;\n}\n\n// when we remove items from an array (via e.g. filtering), we first need to\n// remove the item with the greater index so indexes of other items (that are to\n// be removed from the same array) do not shift. so below is for comparing 2\n// globs if they represent 2 items from the same array.\n\n// example items from same array: ![*][2] ![0][*] ![0][1] ![0][3]\n// should be sorted as ![0][3] ![*][2] ![0][1] ![0][*]\nfunction _compareArrayItemGlobs(a, b) {\n const reANote = re.ARRAY_GLOB_NOTE;\n // both should be negated\n if (!a.isNegated\n || !b.isNegated\n // should be same length (since we're comparing for items in same\n // array)\n || a.notes.length !== b.notes.length\n // last notes should be array brackets\n || !reANote.test(a.last)\n || !reANote.test(b.last)\n // last notes should be different to compare\n || a.last === b.last\n ) return 0;\n\n // negated !..[*] should come last\n if (a.last === '[*]') return 1; // b is first\n if (b.last === '[*]') return -1; // a is first\n\n if (a.parent && b.parent) {\n const { _covers } = Glob;\n if (_covers(a.parent, b.parent, true)) {\n return _compArrIdx(a.last, b.last);\n }\n return 0;\n }\n return _compArrIdx(a.last, b.last);\n}\n\n// x vs !x.*.* » false\n// x vs !x[*] » true\n// x[*] vs !x » true\n// x[*] vs !x[*] » false\n// x.* vs !x.* » false\nfunction _isReverseOf(a, b) {\n return a.isNegated !== b.isNegated\n && a.absGlob === b.absGlob;\n}\n\nfunction _invert(glob) {\n return glob[0] === '!' ? glob.slice(1) : '!' + glob;\n}\n\nconst _rx = /^\\s*!/;\nfunction _negFirstSort(a, b) {\n const negA = _rx.test(a);\n const negB = _rx.test(b);\n if (negA && negB) return a.length >= b.length ? 1 : -1;\n if (negA) return -1;\n if (negB) return 1;\n return 0;\n}\nfunction _negLastSort(a, b) {\n const negA = _rx.test(a);\n const negB = _rx.test(b);\n if (negA && negB) return a.length >= b.length ? 1 : -1;\n if (negA) return 1;\n if (negB) return -1;\n return 0;\n}\n\n// --------------------------------\n// EXPORT\n// --------------------------------\n\nexport { Glob };\n","/* eslint no-use-before-define:0, consistent-return:0, max-statements:0, max-len:0 */\n\nimport { Glob } from './notation.glob';\nimport { NotationError } from './notation.error';\nimport { utils } from '../utils';\n\nconst ERR = {\n SOURCE: 'Invalid source. Expected a data object or array.',\n DEST: 'Invalid destination. Expected a data object or array.',\n NOTATION: 'Invalid notation: ',\n NOTA_OBJ: 'Invalid notations object. ',\n NO_INDEX: 'Implied index does not exist: ',\n NO_PROP: 'Implied property does not exist: '\n};\n\n// created test @ https://regex101.com/r/vLE16M/2\nconst reMATCHER = /(\\[(\\d+|\".*\"|'.*'|`.*`)\\]|[a-z$_][a-z$_\\d]*)/gi;\n// created test @ https://regex101.com/r/fL3PJt/1/\n// /^([a-z$_][a-z$_\\d]*|\\[(\\d+|\".*\"|'.*'|`.*`)\\])(\\[(\\d+|\".*\"|'.*'|`.*`)\\]|(\\.[a-z$_][a-z$_\\d]*))*$/i\nconst reVALIDATOR = new RegExp(\n '^('\n + '[a-z$_][a-z$_\\\\d]*' // JS variable syntax\n + '|' // OR\n + '\\\\[(\\\\d+|\".*\"|\\'.*\\')\\\\]' // array index or object bracket notation\n + ')' // exactly once\n + '('\n + '\\\\[(\\\\d+|\".*\"|\\'.*\\')\\\\]' // followed by same\n + '|' // OR\n + '\\\\.[a-z$_][a-z$_\\\\d]*' // dot, then JS variable syntax\n + ')*' // (both) may repeat any number of times\n + '$'\n , 'i'\n);\n\nconst DEFAULT_OPTS = Object.freeze({\n strict: false,\n preserveIndices: false\n});\n\n/**\n * Notation.js for Node and Browser.\n *\n * Like in most programming languages, JavaScript makes use of dot-notation to\n * access the value of a member of an object (or class). `Notation` class\n * provides various methods for modifying / processing the contents of the\n * given object; by parsing object notation strings or globs.\n *\n * Note that this class will only deal with enumerable properties of the source\n * object; so it should be used to manipulate data objects. It will not deal\n * with preserving the prototype-chain of the given object.\n *\n * @author Onur Yıldırım \n * @license MIT\n */\nclass Notation {\n\n /**\n * Initializes a new instance of `Notation`.\n *\n * @param {Object|Array} [source={}] - The source object (or array) to be\n * notated. Can either be an array or object. If omitted, defaults to an\n * empty object.\n * @param {Object} [options] - Notation options.\n * @param {Boolean} [options.strict=false] - Whether to throw either when\n * a notation path does not exist on the source (i.e. `#get()` and `#remove()`\n * methods); or notation path exists but overwriting is disabled (i.e.\n * `#set()` method). (Note that `.inspectGet()` and `.inspectRemove()` methods\n * are exceptions). It's recommended to set this to `true` and prevent silent\n * failures if you're working with sensitive data. Regardless of `strict` option,\n * it will always throw on invalid notation syntax or other crucial failures.\n *\n * @example\n * const obj = { car: { brand: \"Dodge\", model: \"Charger\", year: 1970 } };\n * const notation = new Notation(obj);\n * notation.get('car.model') // » \"Charger\"\n * notation.remove('car.model').set('car.color', 'red').value\n * // » { car: { brand: \"Dodge\", year: 1970, color: \"red\" } }\n */\n constructor(source, options) {\n if (arguments.length === 0) {\n this._source = {};\n } else if (!utils.isCollection(source)) {\n throw new NotationError(ERR.SOURCE);\n } else {\n this._source = source;\n }\n\n this._isArray = utils.type(this._source) === 'array';\n this.options = options;\n }\n\n // --------------------------------\n // INSTANCE PROPERTIES\n // --------------------------------\n\n /**\n * Gets or sets notation options.\n * @type {Object}\n */\n get options() {\n return this._options;\n }\n\n set options(value) {\n this._options = {\n ...DEFAULT_OPTS,\n ...(this._options || {}),\n ...(value || {})\n };\n }\n\n /**\n * Gets the value of the source object.\n * @type {Object|Array}\n *\n * @example\n * const person = { name: \"Onur\" };\n * const me = Notation.create(person)\n * .set(\"age\", 36)\n * .set(\"car.brand\", \"Ford\")\n * .set(\"car.model\", \"Mustang\")\n * .value;\n * console.log(me); // { name: \"Onur\", age: 36, car: { brand: \"Ford\", model: \"Mustang\" } }\n * console.log(person === me); // true\n */\n get value() {\n return this._source;\n }\n\n // --------------------------------\n // INSTANCE METHODS\n // --------------------------------\n\n /**\n * Recursively iterates through each key of the source object and invokes\n * the given callback function with parameters, on each non-object value.\n *\n * @param {Function} callback - The callback function to be invoked on\n * each on each non-object value. To break out of the loop, return `false`\n * from within the callback.\n * Callback signature: `callback(notation, key, value, object) { ... }`\n *\n * @returns {Notation} - The current `Notation` instance (self).\n *\n * @example\n * const obj = { car: { brand: \"Dodge\", model: \"Charger\", year: 1970 } };\n * Notation.create(obj).each(function (notation, key, value, object) {\n * console.log(notation, value);\n * });\n * // \"car.brand\" \"Dodge\"\n * // \"car.model\" \"Charger\"\n * // \"car.year\" 1970\n */\n each(callback) {\n _each(this._source, callback);\n return this;\n }\n\n /**\n * Iterates through each note of the given notation string by evaluating\n * it on the source object.\n *\n * @param {String} notation - The notation string to be iterated through.\n * @param {Function} callback - The callback function to be invoked on\n * each iteration. To break out of the loop, return `false` from within\n * the callback. Signature: `callback(levelValue, note, index, list)`\n *\n * @returns {Notation} - The current `Notation` instance (self).\n *\n * @example\n * const obj = { car: { brand: \"Dodge\", model: \"Charger\", year: 1970 } };\n * Notation.create(obj)\n * .eachValue(\"car.brand\", function (levelValue, note, index, list) {\n * console.log(note, levelValue); // \"car.brand\" \"Dodge\"\n * });\n */\n eachValue(notation, callback) {\n let level = this._source;\n Notation.eachNote(notation, (levelNotation, note, index, list) => {\n level = utils.hasOwn(level, note) ? level[note] : undefined;\n if (callback(level, levelNotation, note, index, list) === false) return false;\n\n });\n return this;\n }\n\n /**\n * Gets the list of notations from the source object (keys).\n *\n * @returns {Array} - An array of notation strings.\n *\n * @example\n * const obj = { car: { brand: \"Dodge\", model: \"Charger\", year: 1970 } };\n * const notations = Notation.create(obj).getNotations();\n * console.log(notations); // [ \"car.brand\", \"car.model\", \"car.year\" ]\n */\n getNotations() {\n const list = [];\n this.each(notation => {\n list.push(notation);\n });\n return list;\n }\n\n /**\n * Deeply clones the source object. This is also useful if you want to\n * prevent mutating the original source object.\n *\n *
\n * Note that `Notation` expects a data object (or array) with enumerable\n * properties. In addition to plain objects and arrays; supported cloneable\n * property/value types are primitives (such as `String`, `Number`,\n * `Boolean`, `Symbol`, `null` and `undefined`) and built-in types (such as\n * `Date` and `RegExp`).\n *\n * Enumerable properties with types other than these (such as methods,\n * special objects, custom class instances, etc) will be copied by reference.\n * Non-enumerable properties will not be cloned.\n *\n * If you still need full clone support, you can use a library like lodash.\n * e.g. `Notation.create(_.cloneDeep(source))`\n *
\n *\n * @returns {Notation} - The current `Notation` instance (self).\n *\n * @example\n * const mutated = Notation.create(source1).set('newProp', true).value;\n * console.log(source1.newProp); // ——» true\n *\n * const cloned = Notation.create(source2).clone().set('newProp', true).value;\n * console.log('newProp' in source2); // ——» false\n * console.log(cloned.newProp); // ——» true\n */\n clone() {\n this._source = utils.cloneDeep(this._source);\n return this;\n }\n\n /**\n * Flattens the source object to a single-level object with notated keys.\n *\n * @returns {Notation} - The current `Notation` instance (self).\n *\n * @example\n * const obj = { car: { brand: \"Dodge\", model: \"Charger\", year: 1970 } };\n * console.log(Notation.create(obj).flatten().value);\n * // {\n * // \"car.brand\": \"Dodge\",\n * // \"car.model\": \"Charger\",\n * // \"car.year\": 1970\n * // }\n */\n flatten() {\n const o = {};\n this.each((notation, key, value) => {\n o[notation] = value;\n });\n this._source = o;\n return this;\n }\n\n /**\n * Aggregates notated keys of a (single-level) object, and nests them under\n * their corresponding properties. This is the opposite of `Notation#flatten`\n * method. This might be useful when expanding a flat object fetched from\n * a database.\n * @alias Notation#aggregate\n * @chainable\n *\n * @returns {Notation} - The current `Notation` instance (self).\n *\n * @example\n * const obj = { \"car.brand\": \"Dodge\", \"car.model\": \"Charger\", \"car.year\": 1970 }\n * const expanded = Notation.create(obj).expand().value;\n * console.log(expanded); // { car: { brand: \"Dodge\", model: \"Charger\", year: 1970 } };\n */\n expand() {\n this._source = Notation.create({}).merge(this._source).value;\n return this;\n }\n\n /**\n * Alias for `#expand`\n * @private\n * @returns {Notation} -\n */\n aggregate() {\n return this.expand();\n }\n\n /**\n * Inspects the given notation on the source object by checking\n * if the source object actually has the notated property;\n * and getting its value if exists.\n * @param {String} notation - The notation string to be inspected.\n * @returns {InspectResult} - The result object.\n *\n * @example\n * Notation.create({ car: { year: 1970 } }).inspectGet(\"car.year\");\n * // { has: true, value: 1970, lastNote: 'year', lastNoteNormalized: 'year' }\n * Notation.create({ car: { year: 1970 } }).inspectGet(\"car.color\");\n * // { has: false }\n * Notation.create({ car: { color: undefined } }).inspectGet(\"car.color\");\n * // { has: true, value: undefined, lastNote: 'color', lastNoteNormalized: 'color' }\n * Notation.create({ car: { brands: ['Ford', 'Dodge'] } }).inspectGet(\"car.brands[1]\");\n * // { has: true, value: 'Dodge', lastNote: '[1]', lastNoteNormalized: 1 }\n */\n inspectGet(notation) {\n let level = this._source;\n let result = { has: false, value: undefined };\n let parent;\n Notation.eachNote(notation, (levelNotation, note, index) => {\n const lastNoteNormalized = utils.normalizeNote(note);\n if (utils.hasOwn(level, lastNoteNormalized)) {\n level = level[lastNoteNormalized];\n parent = level;\n result = {\n notation,\n has: true,\n value: level,\n type: utils.type(level),\n level: index + 1,\n lastNote: note,\n lastNoteNormalized\n };\n } else {\n // level = undefined;\n result = {\n notation,\n has: false,\n type: 'undefined',\n level: index + 1,\n lastNote: note,\n lastNoteNormalized\n };\n return false; // break out\n }\n });\n\n if (parent === undefined || (result.has && parent === result.value)) parent = this._source;\n result.parentIsArray = utils.type(parent) === 'array';\n\n return result;\n }\n\n /**\n * Notation inspection result object.\n * @typedef Notation~InspectResult\n * @type Object\n * @property {String} notation - Notation that is inspected.\n * @property {Boolean} has - Indicates whether the source object has the\n * given notation as a (leveled) enumerable property. If the property\n * exists but has a value of `undefined`, this will still return `true`.\n * @property {*} value - The value of the notated property. If the source\n * object does not have the notation, the value will be `undefined`.\n * @property {String} type - The type of the notated property. If the source\n * object does not have the notation, the type will be `\"undefined\"`.\n * @property {String} lastNote - Last note of the notation, if actually\n * exists. For example, last note of `'a.b.c'` is `'c'`.\n * @property {String|Number} lastNoteNormalized - Normalized representation\n * of the last note of the notation, if actually exists. For example, last\n * note of `'a.b[1]` is `'[1]'` and will be normalized to number `1`; which\n * indicates an array index.\n * @property {Boolean} parentIsArray - Whether the parent object of the\n * notation path is an array.\n */\n\n /**\n * Inspects and removes the given notation from the source object by\n * checking if the source object actually has the notated property; and\n * getting its value if exists, before removing the property.\n *\n * @param {String} notation - The notation string to be inspected.\n *\n * @returns {InspectResult} - The result object.\n *\n * @example\n * const obj = { name: \"John\", car: { year: 1970 } };\n * let result = Notation.create(obj).inspectRemove(\"car.year\");\n * // result » { notation: \"car.year\", has: true, value: 1970, lastNote: \"year\", lastNoteNormalized: \"year\" }\n * // obj » { name: \"John\", car: {} }\n *\n * result = Notation.create({ car: { year: 1970 } }).inspectRemove(\"car.color\");\n * // result » { notation: \"car.color\", has: false }\n * Notation.create({ car: { color: undefined } }).inspectRemove(\"car['color']\");\n * // { notation: \"car.color\", has: true, value: undefined, lastNote: \"['color']\", lastNoteNormalized: \"color\" }\n *\n * const obj = { car: { colors: [\"black\", \"white\"] } };\n * const result = Notation.create().inspectRemove(\"car.colors[0]\");\n * // result » { notation: \"car.colors[0]\", has: true, value: \"black\", lastNote: \"[0]\", lastNoteNormalized: 0 }\n * // obj » { car: { colors: [(empty), \"white\"] } }\n */\n inspectRemove(notation) {\n if (!notation) throw new Error(ERR.NOTATION + `'${notation}'`);\n const parentNotation = Notation.parent(notation);\n const parent = parentNotation ? this.get(parentNotation, null) : this._source;\n const parentIsArray = utils.type(parent) === 'array';\n const notes = Notation.split(notation);\n const lastNote = notes[notes.length - 1];\n const lastNoteNormalized = utils.normalizeNote(lastNote);\n\n let result, value;\n if (utils.hasOwn(parent, lastNoteNormalized)) {\n value = parent[lastNoteNormalized];\n result = {\n notation,\n has: true,\n value,\n type: utils.type(value),\n level: notes.length,\n lastNote,\n lastNoteNormalized,\n parentIsArray\n };\n\n // if `preserveIndices` is enabled and this is an array, we'll\n // splice the item out. otherwise, we'll use `delete` syntax to\n // empty the item.\n if (!this.options.preserveIndices && parentIsArray) {\n parent.splice(lastNoteNormalized, 1);\n } else {\n delete parent[lastNoteNormalized];\n }\n } else {\n result = {\n notation,\n has: false,\n type: 'undefined',\n level: notes.length,\n lastNote,\n lastNoteNormalized,\n parentIsArray\n };\n }\n\n return result;\n }\n\n /**\n * Checks whether the source object has the given notation\n * as a (leveled) enumerable property. If the property exists\n * but has a value of `undefined`, this will still return `true`.\n * @param {String} notation - The notation string to be checked.\n * @returns {Boolean} -\n *\n * @example\n * Notation.create({ car: { year: 1970 } }).has(\"car.year\"); // true\n * Notation.create({ car: { year: undefined } }).has(\"car.year\"); // true\n * Notation.create({}).has(\"car.color\"); // false\n */\n has(notation) {\n return this.inspectGet(notation).has;\n }\n\n /**\n * Checks whether the source object has the given notation\n * as a (leveled) defined enumerable property. If the property\n * exists but has a value of `undefined`, this will return `false`.\n * @param {String} notation - The notation string to be checked.\n * @returns {Boolean} -\n *\n * @example\n * Notation.create({ car: { year: 1970 } }).hasDefined(\"car.year\"); // true\n * Notation.create({ car: { year: undefined } }).hasDefined(\"car.year\"); // false\n * Notation.create({}).hasDefined(\"car.color\"); // false\n */\n hasDefined(notation) {\n return this.inspectGet(notation).value !== undefined;\n }\n\n /**\n * Gets the value of the corresponding property at the given notation.\n *\n * @param {String} notation - The notation string to be processed.\n * @param {String} [defaultValue] - The default value to be returned if the\n * property is not found or enumerable.\n *\n * @returns {*} - The value of the notated property.\n * @throws {NotationError} - If `strict` option is enabled, `defaultValue`\n * is not set and notation does not exist.\n *\n * @example\n * Notation.create({ car: { brand: \"Dodge\" } }).get(\"car.brand\"); // \"Dodge\"\n * Notation.create({ car: {} }).get(\"car.model\", \"Challenger\"); // \"Challenger\"\n * Notation.create({ car: { model: undefined } }).get(\"car.model\", \"Challenger\"); // undefined\n *\n * @example get value when strict option is enabled\n * // strict option defaults to false\n * Notation.create({ car: {} }).get(\"car.model\"); // undefined\n * Notation.create({ car: {} }, { strict: false }).get(\"car.model\"); // undefined\n * // below will throw bec. strict = true, car.model does not exist\n * // and no default value is given.\n * Notation.create({ car: {} }, { strict: true }).get(\"car.model\");\n */\n get(notation, defaultValue) {\n const result = this.inspectGet(notation);\n // if strict and no default value is set, check if implied index or prop\n // exists\n if (this.options.strict && arguments.length < 2 && !result.has) {\n const msg = result.parentIsArray ? ERR.NO_INDEX : ERR.NO_PROP;\n throw new NotationError(msg + `'${notation}'`);\n }\n return result.has ? result.value : defaultValue;\n }\n\n /**\n * Sets the value of the corresponding property at the given notation. If\n * the property does not exist, it will be created and nested at the\n * calculated level. If it exists; its value will be overwritten by\n * default.\n * @chainable\n *\n * @param {String} notation - The notation string to be processed.\n * @param {*} value - The value to be set for the notated property.\n * @param {String|Boolean} [mode=\"overwrite\"] - Write mode. By default,\n * this is set to `\"overwrite\"` which sets the value by overwriting the\n * target object property or array item at index. To insert an array item\n * (by shifting the index, instead of overwriting); set to `\"insert\"`. To\n * prevent overwriting the value if exists, explicitly set to `false`.\n *\n * @returns {Notation} - The current `Notation` instance (self).\n *\n * @throws {NotationError} - If strict notation is enabled, `overwrite`\n * option is set to `false` and attempted to overwrite an existing value.\n *\n * @example\n * const obj = { car: { brand: \"Dodge\", year: 1970 } };\n * Notation.create(obj)\n * .set(\"car.brand\", \"Ford\")\n * .set(\"car.model\", \"Mustang\")\n * .set(\"car.year\", 1965, false)\n * .set(\"car.color\", \"red\")\n * .set(\"boat\", \"none\");\n * console.log(obj);\n * // { notebook: \"Mac\", car: { brand: \"Ford\", model: \"Mustang\", year: 1970, color: \"red\" }, boat: \"none\" };\n */\n set(notation, value, mode = 'overwrite') {\n if (!notation.trim()) throw new NotationError(ERR.NOTATION + `'${notation}'`);\n if (mode === true) mode = 'overwrite';\n let level = this._source;\n let currentIsLast, nCurrentNote, nNextNote, nextIsArrayNote, type;\n const insertErrMsg = 'Cannot set value by inserting at index, on an object';\n\n Notation.eachNote(notation, (levelNotation, note, index, list) => {\n currentIsLast = index === list.length - 1;\n nCurrentNote = nNextNote || utils.normalizeNote(note);\n nNextNote = currentIsLast ? null : utils.normalizeNote(list[index + 1]);\n type = utils.type(level);\n\n if (type === 'array' && typeof nCurrentNote !== 'number') {\n const parent = Notation.parent(levelNotation) || 'source';\n throw new NotationError(`Cannot set string key '${note}' on array ${parent}`);\n }\n\n // check if the property is at this level\n if (utils.hasOwn(level, nCurrentNote, type)) {\n // check if we're at the last level\n if (currentIsLast) {\n // if mode is \"overwrite\", assign the value.\n if (mode === 'overwrite') {\n level[nCurrentNote] = value;\n } else if (mode === 'insert') {\n if (type === 'array') {\n level.splice(nCurrentNote, 0, value);\n } else {\n throw new NotationError(insertErrMsg);\n }\n }\n // otherwise, will not overwrite\n } else {\n // if not last level; just re-reference the current level.\n level = level[nCurrentNote];\n }\n } else {\n if (currentIsLast && type !== 'array' && mode === 'insert') {\n throw new NotationError(insertErrMsg);\n }\n\n // if next normalized note is a number, it indicates that the\n // current note is actually an array.\n nextIsArrayNote = typeof nNextNote === 'number';\n\n // we don't have this property at this level so; if this is the\n // last level, we set the value if not, we set an empty\n // collection for the next level\n level[nCurrentNote] = (currentIsLast ? value : (nextIsArrayNote ? [] : {}));\n level = level[nCurrentNote];\n }\n });\n return this;\n }\n\n /**\n * Just like the `.set()` method but instead of a single notation\n * string, an object of notations and values can be passed.\n * Sets the value of each corresponding property at the given\n * notation. If a property does not exist, it will be created\n * and nested at the calculated level. If it exists; its value\n * will be overwritten by default.\n * @chainable\n *\n * @param {Object} notationsObject - The notations object to be processed.\n * This can either be a regular object with non-dotted keys\n * (which will be merged to the first/root level of the source object);\n * or a flattened object with notated (dotted) keys.\n * @param {Boolean} [overwrite=true] - Whether to overwrite a property if\n * exists.\n *\n * @returns {Notation} - The current `Notation` instance (self).\n *\n * @example\n * const obj = { car: { brand: \"Dodge\", year: 1970 } };\n * Notation.create(obj).merge({\n * \"car.brand\": \"Ford\",\n * \"car.model\": \"Mustang\",\n * \"car.year\": 1965,\n * \"car.color\": \"red\",\n * \"boat\": \"none\"\n * });\n * console.log(obj);\n * // { car: { brand: \"Ford\", model: \"Mustang\", year: 1970, color: \"red\" }, boat: \"none\" };\n */\n merge(notationsObject, overwrite = true) {\n if (utils.type(notationsObject) !== 'object') {\n throw new NotationError(ERR.NOTA_OBJ + 'Expected an object.');\n }\n let value;\n utils.each(Object.keys(notationsObject), notation => {\n value = notationsObject[notation];\n this.set(notation, value, overwrite);\n });\n return this;\n }\n\n /**\n * Removes the properties by the given list of notations from the source\n * object and returns a new object with the removed properties.\n * Opposite of `merge()` method.\n *\n * @param {Array} notations - The notations array to be processed.\n *\n * @returns {Object} - An object with the removed properties.\n *\n * @example\n * const obj = { car: { brand: \"Dodge\", year: 1970 }, notebook: \"Mac\" };\n * const separated = Notation.create(obj).separate([\"car.brand\", \"boat\" ]);\n * console.log(separated);\n * // { notebook: \"Mac\", car: { brand: \"Ford\" } };\n * console.log(obj);\n * // { car: { year: 1970 } };\n */\n separate(notations) {\n if (utils.type(notations) !== 'array') {\n throw new NotationError(ERR.NOTA_OBJ + 'Expected an array.');\n }\n const o = new Notation({});\n utils.each(notations, notation => {\n const result = this.inspectRemove(notation);\n o.set(notation, result.value);\n });\n this._source = o._source;\n return this;\n }\n\n /**\n * Deep clones the source object while filtering its properties by the\n * given glob notations. Includes all matched properties and removes\n * the rest.\n *\n * The difference between regular notations and glob-notations is that;\n * with the latter, you can use wildcard stars (*) and negate the notation\n * by prepending a bang (!). A negated notation will be excluded.\n *\n * Order of the globs does not matter; they will be logically sorted. Loose\n * globs will be processed first and verbose globs or normal notations will\n * be processed last. e.g. `[ \"car.model\", \"*\", \"!car.*\" ]` will be\n * normalized and sorted as `[ \"*\", \"!car\" ]`.\n *\n * Passing no parameters or passing a glob of `\"!*\"` or `[\"!*\"]` will empty\n * the source object. See `Notation.Glob` class for more information.\n * @chainable\n *\n * @param {Array|String} globList - Glob notation list to be processed.\n * @param {Object} [options] - Filter options.\n * @param {Boolean} [options.restrictive=false] - Whether negated items\n * strictly remove every match. Note that, regardless of this option, if\n * any item has an exact negated version; non-negated is always removed.\n *\n * @returns {Notation} - The current `Notation` instance (self). To get the\n * filtered value, call `.value` property on the instance.\n *\n * @example\n * const car = { brand: \"Ford\", model: { name: \"Mustang\", year: 1970 } };\n * const n = Notation.create(car);\n *\n * console.log(n.filter([ \"*\", \"!model.year\" ]).value); // { brand: \"Ford\", model: { name: \"Mustang\" } }\n * console.log(n.filter(\"model.name\").value); // { model: { name: \"Mustang\" } }\n * console.log(car); // { brand: \"Ford\", model: { name: \"Mustang\", year: 1970 } }\n * console.log(n.filter().value); // {} // —» equivalent to n.filter(\"\") or n.filter(\"!*\")\n */\n filter(globList, options = {}) {\n const { re } = utils;\n\n // ensure array, normalize and sort the globs in logical order. this\n // also concats the array first (to prevent mutating the original\n // array).\n const globs = Glob.normalize(globList, options.restrictive);\n const len = globs.length;\n const empty = this._isArray ? [] : {};\n\n // if globs is \"\" or [\"\"] or [\"!*\"] or [\"![*]\"] set source to empty and return.\n if (len === 0 || (len === 1 && (!globs[0] || re.NEGATE_ALL.test(globs[0])))) {\n this._source = empty;\n return this;\n }\n\n const cloned = utils.cloneDeep(this.value);\n\n const firstIsWildcard = re.WILDCARD.test(globs[0]);\n // if globs only consist of \"*\" or \"[*]\"; set the \"clone\" as source and\n // return.\n if (len === 1 && firstIsWildcard) {\n this._source = cloned;\n return this;\n }\n\n let filtered;\n // if the first item of sorted globs is \"*\" or \"[*]\" we set the source\n // to the (full) \"copy\" and remove the wildcard from globs (not to\n // re-process).\n if (firstIsWildcard) {\n filtered = new Notation(cloned);\n globs.shift();\n } else {\n // otherwise we set an empty object or array as the source so that\n // we can add notations/properties to it.\n filtered = new Notation(empty);\n }\n\n // iterate through globs\n utils.each(globs, globNotation => {\n // console.log('globNotation', globNotation);\n const g = new Glob(globNotation);\n const { glob, absGlob, isNegated, levels } = g;\n let normalized, emptyValue, eType;\n // check whether the glob ends with `.*` or `[*]` then remove\n // trailing glob note and decide for empty value (if negated). for\n // non-negated, trailing wildcards are already removed by\n // normalization.\n if (absGlob.slice(-2) === '.*') {\n normalized = absGlob.slice(0, -2);\n /* istanbul ignore else */\n if (isNegated) emptyValue = {};\n eType = 'object';\n } else if (absGlob.slice(-3) === '[*]') {\n normalized = absGlob.slice(0, -3);\n /* istanbul ignore else */\n if (isNegated) emptyValue = [];\n eType = 'array';\n } else {\n normalized = absGlob;\n }\n\n // we'll check glob vs value integrity if emptyValue is set; and throw if needed.\n const errGlobIntegrity = `Integrity failed for glob '${glob}'. Cannot set empty ${eType} for '${normalized}' which has a type of `; // ...\n\n // check if remaining normalized glob has no wildcard stars e.g.\n // \"a.b\" or \"!a.b.c\" etc..\n if (re.WILDCARDS.test(normalized) === false) {\n if (isNegated) {\n // inspect and directly remove the notation if negated.\n // we need the inspection for the detailed error below.\n const insRemove = filtered.inspectRemove(normalized);\n // console.log('insRemove', insRemove);\n\n // if original glob had `.*` at the end, it means remove\n // contents (not itself). so we'll set an empty object.\n // meaning `some.prop` (prop) is removed completely but\n // `some.prop.*` (prop) results in `{}`. For array notation\n // (`[*]`), we'll set an empty array.\n if (emptyValue) {\n // e.g. for glob `![0].x.*` we expect to set `[0].x = {}`\n // but if `.x` is not an object (or array), we should fail.\n const vType = insRemove.type;\n const errMsg = errGlobIntegrity + `'${vType}'.`;\n // in non-strict mode, only exceptions are `null` and\n // `undefined`, for which we won't throw but we'll not\n // set an empty obj/arr either.\n\n const isValSet = utils.isset(insRemove.value);\n // on critical type mismatch we throw\n // or if original value is undefined or null in strict mode we throw\n if ((isValSet && vType !== eType) || (!isValSet && this.options.strict)) {\n throw new NotationError(errMsg);\n }\n // if parent is an array, we'll insert the value at\n // index bec. we've removed the item and indexes are\n // shifted. Otherwise, we'll simply overwrite the\n // object property value.\n const setMode = insRemove.parentIsArray ? 'insert' : 'overwrite';\n // console.log('setting', normalized, emptyValue, setMode);\n filtered.set(normalized, emptyValue, setMode);\n }\n } else {\n // directly set the same notation from the original\n const insGet = this.inspectGet(normalized); // Notation.create(original).inspectGet ...\n /* istanbul ignore else */\n if (insGet.has) filtered.set(normalized, insGet.value, 'overwrite');\n }\n // move to the next\n return true;\n }\n\n // if glob has wildcard(s), we'll iterate through keys of the source\n // object and see if (full) notation of each key matches the current\n // glob.\n\n // important! we will iterate with eachRight to prevent shifted\n // indexes when removing items from arrays.\n const reverseIterateIfArray = true;\n\n _each(this._source, (originalNotation, key, value) => {\n const originalIsCovered = Glob.create(normalized).covers(originalNotation);\n // console.log('» normalized:', normalized, 'covers', originalNotation, '»', originalIsCovered);\n if (!originalIsCovered) return true; // break\n\n if (this.options.strict && emptyValue) {\n // since original is covered and we have emptyValue set (due\n // to trailing wildcard), here we'll check value vs glob\n // integrity; (only if we're in strict mode).\n\n const vType = utils.type(value);\n // types and number of levels are the same?\n if (vType !== eType\n // we subtract 1 from number of levels bec. the last\n // note is removed since we have emptyValue set.\n && Notation.split(originalNotation).length === levels.length - 1) {\n throw new NotationError(errGlobIntegrity + `'${vType}'.`);\n }\n }\n\n // iterating each note of original notation. i.e.:\n // note1.note2.note3 is iterated from left to right, as:\n // 'note1', 'note1.note2', 'note1.note2.note3' — in order.\n Notation.eachNote(originalNotation, levelNotation => {\n // console.log(' level »', glob, 'covers', levelNotation, '»', g.test(levelNotation));\n\n if (g.test(levelNotation)) {\n const levelLen = Notation.split(levelNotation).length;\n /* istanbul ignore else */\n if (isNegated && levels.length <= levelLen) {\n // console.log(' » removing', levelNotation, 'of', originalNotation);\n filtered.remove(levelNotation);\n // we break and return early if removed bec. e.g.\n // when 'note1.note2' (parent) of\n // 'note1.note2.note3' is also removed, we no more\n // have 'note3'.\n return false;\n }\n // console.log(' » setting', levelNotation, '=', value);\n filtered.set(levelNotation, value, 'overwrite');\n }\n });\n }, reverseIterateIfArray);\n });\n // finally set the filtered's value as the source of our instance and\n // return.\n this._source = filtered.value;\n return this;\n }\n\n /**\n * Removes the property from the source object, at the given notation.\n * @alias Notation#delete\n * @chainable\n * @param {String} notation - The notation to be inspected.\n * @returns {Notation} - The current `Notation` instance (self).\n * @throws {NotationError} - If `strict` option is enabled and notation\n * does not exist.\n *\n * @example\n * const obj = { notebook: \"Mac\", car: { model: \"Mustang\" } };\n * Notation.create(obj).remove(\"car.model\");\n * console.log(obj); // { notebook: \"Mac\", car: { } }\n */\n remove(notation) {\n const result = this.inspectRemove(notation);\n // if strict, check if implied index or prop exists\n if (this.options.strict && !result.has) {\n const msg = result.parentIsArray ? ERR.NO_INDEX : ERR.NO_PROP;\n throw new NotationError(msg + `'${notation}'`);\n }\n return this;\n }\n\n /**\n * Alias of `Notation#remove`\n * @private\n * @param {String} notation -\n * @returns {Notation} -\n */\n delete(notation) {\n this.remove(notation);\n return this;\n }\n\n /**\n * Copies the notated property from the source collection and adds it to the\n * destination — only if the source object actually has that property.\n * This is different than a property with a value of `undefined`.\n * @chainable\n *\n * @param {Object|Array} destination - The destination object that the notated\n * properties will be copied to.\n * @param {String} notation - The notation to get the corresponding property\n * from the source object.\n * @param {String} [newNotation=null] - The notation to set the source property\n * on the destination object. In other words, the copied property will be\n * renamed to this value before set on the destination object. If not set,\n * `notation` argument will be used.\n * @param {Boolean} [overwrite=true] - Whether to overwrite the property on\n * the destination object if it exists.\n *\n * @returns {Notation} - The current `Notation` instance (self).\n *\n * @throws {NotationError} - If `destination` is not a valid collection.\n * @throws {NotationError} - If `notation` or `newNotation` is invalid.\n *\n * @example\n * const obj = { car: { brand: \"Ford\", model: \"Mustang\" } };\n * const models = { dodge: \"Charger\" };\n * Notation.create(obj).copyTo(models, \"car.model\", \"ford\");\n * console.log(models);\n * // { dodge: \"Charger\", ford: \"Mustang\" }\n * // source object (obj) is not modified\n */\n copyTo(destination, notation, newNotation = null, overwrite = true) {\n if (!utils.isCollection(destination)) throw new NotationError(ERR.DEST);\n const result = this.inspectGet(notation);\n if (result.has) {\n const newN = utils.getNewNotation(newNotation, notation);\n Notation.create(destination).set(newN, result.value, overwrite);\n }\n return this;\n }\n\n /**\n * Copies the notated property from the target collection and adds it to\n * (own) source object — only if the target object actually has that\n * property. This is different than a property with a value of `undefined`.\n * @chainable\n *\n * @param {Object|Array} target - The target collection that the notated\n * properties will be copied from.\n * @param {String} notation - The notation to get the corresponding\n * property from the target object.\n * @param {String} [newNotation=null] - The notation to set the copied\n * property on our source collection. In other words, the copied property\n * will be renamed to this value before set. If not set, `notation`\n * argument will be used.\n * @param {Boolean} [overwrite=true] - Whether to overwrite the property on\n * our collection if it exists.\n *\n * @returns {Notation} - The current `Notation` instance (self).\n *\n * @throws {NotationError} - If `target` is not a valid collection.\n * @throws {NotationError} - If `notation` or `newNotation` is invalid.\n *\n * @example\n * const obj = { car: { brand: \"Ford\", model: \"Mustang\" } };\n * const models = { dodge: \"Charger\" };\n * Notation.create(obj).copyFrom(models, \"dodge\", \"car.model\", true);\n * console.log(obj);\n * // { car: { brand: \"Ford\", model: \"Charger\" } }\n * // models object is not modified\n */\n copyFrom(target, notation, newNotation = null, overwrite = true) {\n if (!utils.isCollection(target)) throw new NotationError(ERR.DEST);\n const result = Notation.create(target).inspectGet(notation);\n if (result.has) {\n const newN = utils.getNewNotation(newNotation, notation);\n this.set(newN, result.value, overwrite);\n }\n return this;\n }\n\n /**\n * Removes the notated property from the source (own) collection and adds\n * it to the destination — only if the source collection actually has that\n * property. This is different than a property with a value of `undefined`.\n * @chainable\n *\n * @param {Object|Array} destination - The destination collection that the\n * notated properties will be moved to.\n * @param {String} notation - The notation to get the corresponding\n * property from the source object.\n * @param {String} [newNotation=null] - The notation to set the source\n * property on the destination object. In other words, the moved property\n * will be renamed to this value before set on the destination object. If\n * not set, `notation` argument will be used.\n * @param {Boolean} [overwrite=true] - Whether to overwrite the property on\n * the destination object if it exists.\n *\n * @returns {Notation} - The current `Notation` instance (self).\n *\n * @throws {NotationError} - If `destination` is not a valid collection.\n * @throws {NotationError} - If `notation` or `newNotation` is invalid.\n *\n * @example\n * const obj = { car: { brand: \"Ford\", model: \"Mustang\" } };\n * const models = { dodge: \"Charger\" };\n * Notation.create(obj).moveTo(models, \"car.model\", \"ford\");\n * console.log(obj);\n * // { car: { brand: \"Ford\" } }\n * console.log(models);\n * // { dodge: \"Charger\", ford: \"Mustang\" }\n */\n moveTo(destination, notation, newNotation = null, overwrite = true) {\n if (!utils.isCollection(destination)) throw new NotationError(ERR.DEST);\n const result = this.inspectRemove(notation);\n if (result.has) {\n const newN = utils.getNewNotation(newNotation, notation);\n Notation.create(destination).set(newN, result.value, overwrite);\n }\n return this;\n }\n\n /**\n * Removes the notated property from the target collection and adds it to (own)\n * source collection — only if the target object actually has that property.\n * This is different than a property with a value of `undefined`.\n * @chainable\n *\n * @param {Object|Array} target - The target collection that the notated\n * properties will be moved from.\n * @param {String} notation - The notation to get the corresponding property\n * from the target object.\n * @param {String} [newNotation=null] - The notation to set the target\n * property on the source object. In other words, the moved property\n * will be renamed to this value before set on the source object.\n * If not set, `notation` argument will be used.\n * @param {Boolean} [overwrite=true] - Whether to overwrite the property on\n * the source object if it exists.\n *\n * @returns {Notation} - The current `Notation` instance (self).\n *\n * @throws {NotationError} - If `target` is not a valid collection.\n * @throws {NotationError} - If `notation` or `newNotation` is invalid.\n *\n * @example\n * const obj = { car: { brand: \"Ford\", model: \"Mustang\" } };\n * const models = { dodge: \"Charger\" };\n * Notation.create(obj).moveFrom(models, \"dodge\", \"car.model\", true);\n * console.log(obj);\n * // { car: { brand: \"Ford\", model: \"Charger\" } }\n * console.log(models);\n * // {}\n */\n moveFrom(target, notation, newNotation = null, overwrite = true) {\n if (!utils.isCollection(target)) throw new NotationError(ERR.DEST);\n const result = Notation.create(target).inspectRemove(notation);\n if (result.has) {\n const newN = utils.getNewNotation(newNotation, notation);\n this.set(newN, result.value, overwrite);\n }\n return this;\n }\n\n /**\n * Renames the notated property of the source collection by the new notation.\n * @alias Notation#renote\n * @chainable\n *\n * @param {String} notation - The notation to get the corresponding\n * property (value) from the source collection.\n * @param {String} newNotation - The new notation for the targeted\n * property value. If not set, the source collection will not be modified.\n * @param {Boolean} [overwrite=true] - Whether to overwrite the property at\n * the new notation, if it exists.\n *\n * @returns {Notation} - The current `Notation` instance (self).\n *\n * @throws {NotationError} - If `notation` or `newNotation` is invalid.\n *\n * @example\n * const obj = { car: { brand: \"Ford\", model: \"Mustang\" } };\n * Notation.create(obj)\n * .rename(\"car.brand\", \"carBrand\")\n * .rename(\"car.model\", \"carModel\");\n * console.log(obj);\n * // { carBrand: \"Ford\", carModel: \"Mustang\" }\n */\n rename(notation, newNotation, overwrite) {\n return this.moveTo(this._source, notation, newNotation, overwrite);\n }\n\n /**\n * Alias for `#rename`\n * @private\n * @param {String} notation -\n * @param {String} newNotation -\n * @param {Boolean} [overwrite=true] -\n * @returns {Notation} -\n */\n renote(notation, newNotation, overwrite) {\n return this.rename(notation, newNotation, overwrite);\n }\n\n /**\n * Extracts the property at the given notation to a new object by copying\n * it from the source collection. This is equivalent to `.copyTo({},\n * notation, newNotation)`.\n * @alias Notation#copyToNew\n *\n * @param {String} notation - The notation to get the corresponding\n * property (value) from the source object.\n * @param {String} newNotation - The new notation to be set on the new\n * object for the targeted property value. If not set, `notation` argument\n * will be used.\n *\n * @returns {Object} - Returns a new object with the notated property.\n *\n * @throws {NotationError} - If `notation` or `newNotation` is invalid.\n *\n * @example\n * const obj = { car: { brand: \"Ford\", model: \"Mustang\" } };\n * const extracted = Notation.create(obj).extract(\"car.brand\", \"carBrand\");\n * console.log(extracted);\n * // { carBrand: \"Ford\" }\n * // obj is not modified\n */\n extract(notation, newNotation) {\n const o = {};\n this.copyTo(o, notation, newNotation);\n return o;\n }\n\n /**\n * Alias for `#extract`\n * @private\n * @param {String} notation -\n * @param {String} newNotation -\n * @returns {Object} -\n */\n copyToNew(notation, newNotation) {\n return this.extract(notation, newNotation);\n }\n\n /**\n * Extrudes the property at the given notation to a new collection by\n * moving it from the source collection. This is equivalent to `.moveTo({},\n * notation, newNotation)`.\n * @alias Notation#moveToNew\n *\n * @param {String} notation - The notation to get the corresponding\n * property (value) from the source object.\n * @param {String} newNotation - The new notation to be set on the new\n * object for the targeted property value. If not set, `notation` argument\n * will be used.\n *\n * @returns {Object} - Returns a new object with the notated property.\n *\n * @example\n * const obj = { car: { brand: \"Ford\", model: \"Mustang\" } };\n * const extruded = Notation.create(obj).extrude(\"car.brand\", \"carBrand\");\n * console.log(obj);\n * // { car: { model: \"Mustang\" } }\n * console.log(extruded);\n * // { carBrand: \"Ford\" }\n */\n extrude(notation, newNotation) {\n const o = {};\n this.moveTo(o, notation, newNotation);\n return o;\n }\n\n /**\n * Alias for `#extrude`\n * @private\n * @param {String} notation -\n * @param {String} newNotation -\n * @returns {Object} -\n */\n moveToNew(notation, newNotation) {\n return this.extrude(notation, newNotation);\n }\n\n // --------------------------------\n // STATIC MEMBERS\n // --------------------------------\n\n /**\n * Basically constructs a new `Notation` instance.\n * @chainable\n * @param {Object|Array} [source={}] - The source collection to be notated.\n * @param {Object} [options] - Notation options.\n * @param {Boolean} [options.strict=false] - Whether to throw when a\n * notation path does not exist on the source. (Note that `.inspectGet()`\n * and `.inspectRemove()` methods are exceptions). It's recommended to\n * set this to `true` and prevent silent failures if you're working\n * with sensitive data. Regardless of `strict` option, it will always\n * throw on invalid notation syntax or other crucial failures.\n *\n * @returns {Notation} - The created instance.\n *\n * @example\n * const obj = { car: { brand: \"Dodge\", model: \"Charger\", year: 1970 } };\n * const notation = Notation.create(obj); // equivalent to new Notation(obj)\n * notation.get('car.model') // » \"Charger\"\n * notation.remove('car.model').set('car.color', 'red').value\n * // » { car: { brand: \"Dodge\", year: 1970, color: \"red\" } }\n */\n static create(source, options) {\n if (arguments.length === 0) {\n return new Notation({});\n }\n return new Notation(source, options);\n }\n\n /**\n * Checks whether the given notation string is valid. Note that the star\n * (`*`) (which is a valid character, even if irregular) is NOT treated as\n * wildcard here. This checks for regular dot-notation, not a glob-notation.\n * For glob notation validation, use `Notation.Glob.isValid()` method. Same\n * goes for the negation character/prefix (`!`).\n *\n * @param {String} notation - The notation string to be checked.\n * @returns {Boolean} -\n *\n * @example\n * Notation.isValid('prop1.prop2.prop3'); // true\n * Notation.isValid('x'); // true\n * Notation.isValid('x.arr[0].y'); // true\n * Notation.isValid('x[\"*\"]'); // true\n * Notation.isValid('x.*'); // false (this would be valid for Notation#filter() only or Notation.Glob class)\n * Notation.isValid('@1'); // false (should be \"['@1']\")\n * Notation.isValid(null); // false\n */\n static isValid(notation) {\n return typeof notation === 'string' && reVALIDATOR.test(notation);\n }\n\n /**\n * Splits the given notation string into its notes (levels).\n * @param {String} notation Notation string to be splitted.\n * @returns {Array} - A string array of notes (levels).\n * @throws {NotationError} - If given notation is invalid.\n */\n static split(notation) {\n if (!Notation.isValid(notation)) {\n throw new NotationError(ERR.NOTATION + `'${notation}'`);\n }\n return notation.match(reMATCHER);\n }\n\n /**\n * Joins the given notes into a notation string.\n * @param {String} notes Notes (levels) to be joined.\n * @returns {String} Joined notation string.\n */\n static join(notes) {\n return utils.joinNotes(notes);\n }\n\n /**\n * Counts the number of notes/levels in the given notation.\n * @alias Notation.countLevels\n * @param {String} notation - The notation string to be processed.\n * @returns {Number} - Number of notes.\n * @throws {NotationError} - If given notation is invalid.\n */\n static countNotes(notation) {\n return Notation.split(notation).length;\n }\n\n /**\n * Alias of `Notation.countNotes`.\n * @private\n * @param {String} notation -\n * @returns {Number} -\n */\n static countLevels(notation) {\n return Notation.countNotes(notation);\n }\n\n /**\n * Gets the first (root) note of the notation string.\n * @param {String} notation - The notation string to be processed.\n * @returns {String} - First note.\n * @throws {NotationError} - If given notation is invalid.\n *\n * @example\n * Notation.first('first.prop2.last'); // \"first\"\n */\n static first(notation) {\n return Notation.split(notation)[0];\n }\n\n /**\n * Gets the last note of the notation string.\n * @param {String} notation - The notation string to be processed.\n * @returns {String} - Last note.\n * @throws {NotationError} - If given notation is invalid.\n *\n * @example\n * Notation.last('first.prop2.last'); // \"last\"\n */\n static last(notation) {\n const list = Notation.split(notation);\n return list[list.length - 1];\n }\n\n /**\n * Gets the parent notation (up to but excluding the last note)\n * from the notation string.\n * @param {String} notation - The notation string to be processed.\n * @returns {String} - Parent note if any. Otherwise, `null`.\n * @throws {NotationError} - If given notation is invalid.\n *\n * @example\n * Notation.parent('first.prop2.last'); // \"first.prop2\"\n * Notation.parent('single'); // null\n */\n static parent(notation) {\n const last = Notation.last(notation);\n return notation.slice(0, -last.length).replace(/\\.$/, '') || null;\n }\n\n /**\n * Iterates through each note/level of the given notation string.\n * @alias Notation.eachLevel\n *\n * @param {String} notation - The notation string to be iterated through.\n * @param {Function} callback - The callback function to be invoked on\n * each iteration. To break out of the loop, return `false` from within the\n * callback.\n * Callback signature: `callback(levelNotation, note, index, list) { ... }`\n *\n * @returns {void}\n * @throws {NotationError} - If given notation is invalid.\n *\n * @example\n * const notation = 'first.prop2.last';\n * Notation.eachNote(notation, function (levelNotation, note, index, list) {\n * console.log(index, note, levelNotation);\n * });\n * // 0 \"first\" \"first\"\n * // 1 \"first.prop2\" \"prop2\"\n * // 2 \"first.prop2.last\" \"last\"\n */\n static eachNote(notation, callback) {\n const notes = Notation.split(notation);\n const levelNotes = [];\n utils.each(notes, (note, index) => {\n levelNotes.push(note);\n if (callback(Notation.join(levelNotes), note, index, notes) === false) return false;\n }, Notation);\n }\n\n /**\n * Alias of `Notation.eachNote`.\n * @private\n * @param {String} notation -\n * @param {Function} callback -\n * @returns {void}\n */\n static eachLevel(notation, callback) {\n Notation.eachNote(notation, callback);\n }\n\n}\n\n/**\n * Error class specific to `Notation`.\n * @private\n *\n * @class\n * @see `{@link #Notation.Error}`\n */\nNotation.Error = NotationError;\n\n/**\n * Utility for validating, comparing and sorting dot-notation globs.\n * This is internally used by `Notation` class.\n * @private\n *\n * @class\n * @see `{@link #Notation.Glob}`\n */\nNotation.Glob = Glob;\n\n/**\n * Undocumented\n * @private\n */\nNotation.utils = utils;\n\n// --------------------------------\n// HELPERS\n// --------------------------------\n\n/**\n * Deep iterates through each note (level) of each item in the given\n * collection.\n * @private\n * @param {Object|Array} collection A data object or an array, as the source.\n * @param {Function} callback A function to be executed on each iteration,\n * with the following arguments: `(levelNotation, note, value, collection)`\n * @param {Boolean} [reverseIfArray=false] Set to `true` to iterate with\n * `eachRight` to prevent shifted indexes when removing items from arrays.\n * @param {Boolean} [byLevel=false] Indicates whether to iterate notations by\n * each level or by the end value. For example, if we have a collection of\n * `{a: { b: true } }`, and `byLevel` is set; the callback will be invoked on\n * the following notations: `a`, `a.b`. Otherwise, it will be invoked only on\n * `a.b`.\n * @param {String} [parentNotation] Storage for parent (previous) notation.\n * @param {Collection} [topSource] Storage for initial/main collection.\n * @returns {void}\n */\nfunction _each(collection, callback, reverseIfArray = false, byLevel = false, parentNotation = null, topSource = null) { // eslint-disable-line max-params\n const source = topSource || collection;\n // if (!utils.isCollection(collection)) throw ... // no need\n utils.eachItem(collection, (value, keyOrIndex) => {\n const note = typeof keyOrIndex === 'number'\n ? `[${keyOrIndex}]`\n : keyOrIndex;\n const currentNotation = Notation.join([parentNotation, note]);\n const isCollection = utils.isCollection(value);\n // if it's not a collection we'll execute the callback. if it's a\n // collection but byLevel is set, we'll also execute the callback.\n if (!isCollection || byLevel) {\n if (callback(currentNotation, note, value, source) === false) return false;\n }\n // deep iterating if collection\n if (isCollection) _each(value, callback, reverseIfArray, byLevel, currentNotation, source);\n }, null, reverseIfArray);\n}\n\n// --------------------------------\n// EXPORT\n// --------------------------------\n\nexport { Notation };\n","/* istanbul ignore file */\nexport * from './core/notation';\n","\nimport { NotationError } from './core/notation.error';\n\nconst objProto = Object.prototype;\nconst symValueOf = typeof Symbol === 'function'\n ? Symbol.prototype.valueOf\n /* istanbul ignore next */\n : null;\n\n// never use 'g' (global) flag in regexps below\nconst VAR = /^[a-z$_][a-z$_\\d]*$/i;\nconst ARRAY_NOTE = /^\\[(\\d+)\\]$/;\nconst ARRAY_GLOB_NOTE = /^\\[(\\d+|\\*)\\]$/;\nconst OBJECT_BRACKETS = /^\\[(?:'(.*)'|\"(.*)\"|`(.*)`)\\]$/;\nconst WILDCARD = /^(\\[\\*\\]|\\*)$/;\n// matches `*` and `[*]` if outside of quotes.\nconst WILDCARDS = /(\\*|\\[\\*\\])(?=(?:[^\"]|\"[^\"]*\")*$)(?=(?:[^']|'[^']*')*$)/;\n// matches trailing wildcards at the end of a non-negated glob.\n// e.g. `x.y.*[*].*` » $1 = `x.y`, $2 = `.*[*].*`\nconst NON_NEG_WILDCARD_TRAIL = /^(?!!)(.+?)(\\.\\*|\\[\\*\\])+$/;\nconst NEGATE_ALL = /^!(\\*|\\[\\*\\])$/;\n// ending with '.*' or '[*]'\n\nconst _reFlags = /\\w*$/;\n\nconst utils = {\n\n re: {\n VAR,\n ARRAY_NOTE,\n ARRAY_GLOB_NOTE,\n OBJECT_BRACKETS,\n WILDCARD,\n WILDCARDS,\n NON_NEG_WILDCARD_TRAIL,\n NEGATE_ALL\n },\n\n type(o) {\n return objProto.toString.call(o).match(/\\s(\\w+)/i)[1].toLowerCase();\n },\n\n isCollection(o) {\n const t = utils.type(o);\n return t === 'object' || t === 'array';\n },\n\n isset(o) {\n return o !== undefined && o !== null;\n },\n\n ensureArray(o) {\n if (utils.type(o) === 'array') return o;\n return o === null || o === undefined ? [] : [o];\n },\n\n // simply returning true will get rid of the \"holes\" in the array.\n // e.g. [0, , 1, , undefined, , , 2, , , null].filter(() => true);\n // ——» [0, 1, undefined, 2, null]\n\n // cleanSparseArray(a) {\n // return a.filter(() => true);\n // },\n\n // added _collectionType for optimization (in loops)\n hasOwn(collection, keyOrIndex, _collectionType) {\n if (!collection) return false;\n const isArr = (_collectionType || utils.type(collection)) === 'array';\n if (!isArr && typeof keyOrIndex === 'string') {\n return keyOrIndex && objProto.hasOwnProperty.call(collection, keyOrIndex);\n }\n if (typeof keyOrIndex === 'number') {\n return keyOrIndex >= 0 && keyOrIndex < collection.length;\n }\n return false;\n },\n\n cloneDeep(collection) {\n const t = utils.type(collection);\n switch (t) {\n case 'date':\n return new Date(collection.valueOf());\n case 'regexp': {\n const flags = _reFlags.exec(collection).toString();\n const copy = new collection.constructor(collection.source, flags);\n copy.lastIndex = collection.lastIndex;\n return copy;\n }\n case 'symbol':\n return symValueOf\n ? Object(symValueOf.call(collection))\n /* istanbul ignore next */\n : collection;\n case 'array':\n return collection.map(utils.cloneDeep);\n case 'object': {\n const copy = {};\n // only enumerable string keys\n Object.keys(collection).forEach(k => {\n copy[k] = utils.cloneDeep(collection[k]);\n });\n return copy;\n }\n // primitives copied over by value\n // case 'string':\n // case 'number':\n // case 'boolean':\n // case 'null':\n // case 'undefined':\n default: // others will be referenced\n return collection;\n }\n },\n\n // iterates over elements of an array, executing the callback for each\n // element.\n each(array, callback, thisArg) {\n const len = array.length;\n let index = -1;\n while (++index < len) {\n if (callback.apply(thisArg, [array[index], index, array]) === false) return;\n }\n },\n\n eachRight(array, callback, thisArg) {\n let index = array.length;\n while (index--) {\n if (callback.apply(thisArg, [array[index], index, array]) === false) return;\n }\n },\n\n eachProp(object, callback, thisArg) {\n const keys = Object.keys(object);\n let index = -1;\n while (++index < keys.length) {\n const key = keys[index];\n if (callback.apply(thisArg, [object[key], key, object]) === false) return;\n }\n },\n\n eachItem(collection, callback, thisArg, reverseIfArray = false) {\n if (utils.type(collection) === 'array') {\n // important! we should iterate with eachRight to prevent shifted\n // indexes when removing items from arrays.\n return reverseIfArray\n ? utils.eachRight(collection, callback, thisArg)\n : utils.each(collection, callback, thisArg);\n }\n return utils.eachProp(collection, callback, thisArg);\n },\n\n pregQuote(str) {\n const re = /[.\\\\+*?[^\\]$(){}=!<>|:-]/g;\n return String(str).replace(re, '\\\\$&');\n },\n\n stringOrArrayOf(o, value) {\n return typeof value === 'string'\n && (o === value\n || (utils.type(o) === 'array' && o.length === 1 && o[0] === value)\n );\n },\n\n hasSingleItemOf(arr, itemValue) {\n return arr.length === 1\n && (arguments.length === 2 ? arr[0] === itemValue : true);\n },\n\n // remove trailing/redundant wildcards if not negated\n removeTrailingWildcards(glob) {\n // return glob.replace(/(.+?)(\\.\\*|\\[\\*\\])*$/, '$1');\n return glob.replace(NON_NEG_WILDCARD_TRAIL, '$1');\n },\n\n normalizeNote(note) {\n if (VAR.test(note)) return note;\n // check array index notation e.g. `[1]`\n let m = note.match(ARRAY_NOTE);\n if (m) return parseInt(m[1], 10);\n // check object bracket notation e.g. `[\"a-b\"]`\n m = note.match(OBJECT_BRACKETS);\n if (m) return (m[1] || m[2] || m[3]);\n throw new NotationError(`Invalid note: '${note}'`);\n },\n\n joinNotes(notes) {\n const lastIndex = notes.length - 1;\n return notes.map((current, i) => {\n if (!current) return '';\n const next = lastIndex >= i + 1 ? notes[i + 1] : null;\n const dot = next\n ? next[0] === '[' ? '' : '.'\n : '';\n return current + dot;\n }).join('');\n },\n\n getNewNotation(newNotation, notation) {\n const errMsg = `Invalid new notation: '${newNotation}'`;\n // note validations (for newNotation and notation) are already made by\n // other methods in the flow.\n let newN;\n if (typeof newNotation === 'string') {\n newN = newNotation.trim();\n if (!newN) throw new NotationError(errMsg);\n return newN;\n }\n if (notation && !utils.isset(newNotation)) return notation;\n throw new NotationError(errMsg);\n }\n\n};\n\nexport { utils };\n"],"sourceRoot":""} \ No newline at end of file diff --git a/lib/notation.min.js b/lib/notation.min.js deleted file mode 100644 index 16ad253..0000000 --- a/lib/notation.min.js +++ /dev/null @@ -1,2 +0,0 @@ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("notation",[],t):"object"==typeof exports?exports.notation=t():e.notation=t()}(this,function(){return r={},o.m=n=[function(e,t,n){"use strict";function u(e){return(u="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function o(i){var a=r();return function(){var e,t,n,r,o=l(i);return t=a?(e=l(this).constructor,Reflect.construct(o,arguments,e)):o.apply(this,arguments),n=this,!(r=t)||"object"!==u(r)&&"function"!=typeof r?s(n):r}}function s(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}function i(e){var r="function"==typeof Map?new Map:void 0;return(i=function(e){if(null===e||(t=e,-1===Function.toString.call(t).indexOf("[native code]")))return e;var t;if("function"!=typeof e)throw new TypeError("Super expression must either be null or a function");if(void 0!==r){if(r.has(e))return r.get(e);r.set(e,n)}function n(){return a(e,arguments,l(this).constructor)}return n.prototype=Object.create(e.prototype,{constructor:{value:n,enumerable:!1,writable:!0,configurable:!0}}),c(n,e)})(e)}function a(e,t,n){return(a=r()?Reflect.construct:function(e,t,n){var r=[null];r.push.apply(r,t);var o=new(Function.bind.apply(e,r));return n&&c(o,n.prototype),o}).apply(null,arguments)}function r(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),!0}catch(e){return!1}}function c(e,t){return(c=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e})(e,t)}function l(e){return(l=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)})(e)}n.r(t),n.d(t,"Notation",function(){return M});var f=Object.setPrototypeOf,k=function(){!function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),t&&c(e,t)}(r,i(Error));var n=o(r);function r(){var e,t=0|:-]/g,"\\$&")},stringOrArrayOf:function(e,t){return"string"==typeof t&&(e===t||"array"===j.type(e)&&1===e.length&&e[0]===t)},hasSingleItemOf:function(e,t){return 1===e.length&&(2!==arguments.length||e[0]===t)},removeTrailingWildcards:function(e){return e.replace(b,"$1")},normalizeNote:function(e){if(v.test(e))return e;var t=e.match(y);if(t)return parseInt(t[1],10);if(t=e.match(g))return t[1]||t[2]||t[3];throw new k("Invalid note: '".concat(e,"'"))},joinNotes:function(r){var o=r.length-1;return r.map(function(e,t){if(!e)return"";var n=t+1<=o?r[t+1]:null;return e+(!n||"["===n[0]?"":".")}).join("")},getNewNotation:function(e,t){var n,r="Invalid new notation: '".concat(e,"'");if("string"==typeof e){if(!(n=e.trim()))throw new k(r);return n}if(t&&!j.isset(e))return t;throw new k(r)}};function w(t,e){var n,r=Object.keys(t);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(t),e&&(n=n.filter(function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable})),r.push.apply(r,n)),r}function O(o){for(var e=1;eu.length)return!1;for(var s=!0,c=r?x:I,l=0;lu.length&&"!"===e[0]||u.length>a.length&&"!"===t[0]?"!":"",c=Math.max(a.length,u.length),l=[],f=0;fr.absGlob?1:0:a=t.length?1:-1:n?-1:r?1:0}function $(e,t){var n=P.test(e),r=P.test(t);return n&&r?e.length>=t.length?1:-1:n?1:r?-1:0}function G(t,e){var n,r=Object.keys(t);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(t),e&&(n=n.filter(function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable})),r.push.apply(r,n)),r}function C(o){for(var e=1;e true);\n // ——» [0, 1, undefined, 2, null]\n\n // cleanSparseArray(a) {\n // return a.filter(() => true);\n // },\n\n // added _collectionType for optimization (in loops)\n hasOwn(collection, keyOrIndex, _collectionType) {\n if (!collection) return false;\n const isArr = (_collectionType || utils.type(collection)) === 'array';\n if (!isArr && typeof keyOrIndex === 'string') {\n return keyOrIndex && objProto.hasOwnProperty.call(collection, keyOrIndex);\n }\n if (typeof keyOrIndex === 'number') {\n return keyOrIndex >= 0 && keyOrIndex < collection.length;\n }\n return false;\n },\n\n cloneDeep(collection) {\n const t = utils.type(collection);\n switch (t) {\n case 'date':\n return new Date(collection.valueOf());\n case 'regexp': {\n const flags = _reFlags.exec(collection).toString();\n const copy = new collection.constructor(collection.source, flags);\n copy.lastIndex = collection.lastIndex;\n return copy;\n }\n case 'symbol':\n return symValueOf\n ? Object(symValueOf.call(collection))\n /* istanbul ignore next */\n : collection;\n case 'array':\n return collection.map(utils.cloneDeep);\n case 'object': {\n const copy = {};\n // only enumerable string keys\n Object.keys(collection).forEach(k => {\n copy[k] = utils.cloneDeep(collection[k]);\n });\n return copy;\n }\n // primitives copied over by value\n // case 'string':\n // case 'number':\n // case 'boolean':\n // case 'null':\n // case 'undefined':\n default: // others will be referenced\n return collection;\n }\n },\n\n // iterates over elements of an array, executing the callback for each\n // element.\n each(array, callback, thisArg) {\n const len = array.length;\n let index = -1;\n while (++index < len) {\n if (callback.apply(thisArg, [array[index], index, array]) === false) return;\n }\n },\n\n eachRight(array, callback, thisArg) {\n let index = array.length;\n while (index--) {\n if (callback.apply(thisArg, [array[index], index, array]) === false) return;\n }\n },\n\n eachProp(object, callback, thisArg) {\n const keys = Object.keys(object);\n let index = -1;\n while (++index < keys.length) {\n const key = keys[index];\n if (callback.apply(thisArg, [object[key], key, object]) === false) return;\n }\n },\n\n eachItem(collection, callback, thisArg, reverseIfArray = false) {\n if (utils.type(collection) === 'array') {\n // important! we should iterate with eachRight to prevent shifted\n // indexes when removing items from arrays.\n return reverseIfArray\n ? utils.eachRight(collection, callback, thisArg)\n : utils.each(collection, callback, thisArg);\n }\n return utils.eachProp(collection, callback, thisArg);\n },\n\n pregQuote(str) {\n const re = /[.\\\\+*?[^\\]$(){}=!<>|:-]/g;\n return String(str).replace(re, '\\\\$&');\n },\n\n stringOrArrayOf(o, value) {\n return typeof value === 'string'\n && (o === value\n || (utils.type(o) === 'array' && o.length === 1 && o[0] === value)\n );\n },\n\n hasSingleItemOf(arr, itemValue) {\n return arr.length === 1\n && (arguments.length === 2 ? arr[0] === itemValue : true);\n },\n\n // remove trailing/redundant wildcards if not negated\n removeTrailingWildcards(glob) {\n // return glob.replace(/(.+?)(\\.\\*|\\[\\*\\])*$/, '$1');\n return glob.replace(NON_NEG_WILDCARD_TRAIL, '$1');\n },\n\n normalizeNote(note) {\n if (VAR.test(note)) return note;\n // check array index notation e.g. `[1]`\n let m = note.match(ARRAY_NOTE);\n if (m) return parseInt(m[1], 10);\n // check object bracket notation e.g. `[\"a-b\"]`\n m = note.match(OBJECT_BRACKETS);\n if (m) return (m[1] || m[2] || m[3]);\n throw new NotationError(`Invalid note: '${note}'`);\n },\n\n joinNotes(notes) {\n const lastIndex = notes.length - 1;\n return notes.map((current, i) => {\n if (!current) return '';\n const next = lastIndex >= i + 1 ? notes[i + 1] : null;\n const dot = next\n ? next[0] === '[' ? '' : '.'\n : '';\n return current + dot;\n }).join('');\n },\n\n getNewNotation(newNotation, notation) {\n const errMsg = `Invalid new notation: '${newNotation}'`;\n // note validations (for newNotation and notation) are already made by\n // other methods in the flow.\n let newN;\n if (typeof newNotation === 'string') {\n newN = newNotation.trim();\n if (!newN) throw new NotationError(errMsg);\n return newN;\n }\n if (notation && !utils.isset(newNotation)) return notation;\n throw new NotationError(errMsg);\n }\n\n};\n\nexport { utils };\n","/* eslint no-use-before-define:0, consistent-return:0, max-statements:0 */\n\nimport { Notation } from './notation';\nimport { NotationError } from './notation.error';\nimport { utils } from '../utils';\n\n// http://www.linfo.org/wildcard.html\n// http://en.wikipedia.org/wiki/Glob_%28programming%29\n// http://en.wikipedia.org/wiki/Wildcard_character#Computing\n\n// created test @ https://regex101.com/r/U08luj/2\nconst reMATCHER = /(\\[(\\d+|\\*|\".*\"|'.*')\\]|[a-z$_][a-z$_\\d]*|\\*)/gi; // ! negation should be removed first\n// created test @ https://regex101.com/r/mC8unE/3\n// /^!?(\\*|[a-z$_][a-z$_\\d]*|\\[(\\d+|\".*\"|'.*'|`.*`|\\*)\\])(\\[(\\d+|\".*\"|'.*'|`.*`|\\*)\\]|\\.[a-z$_][a-z$_\\d]*|\\.\\*)*$/i\nconst reVALIDATOR = new RegExp(\n '^'\n + '!?(' // optional negation, only in the front\n + '\\\\*' // wildcard star\n + '|' // OR\n + '[a-z$_][a-z$_\\\\d]*' // JS variable syntax\n + '|' // OR\n + '\\\\[(\\\\d+|\\\\*|\".*\"|\\'.*\\')\\\\]' // array index or wildcard, or object bracket notation\n + ')' // exactly once\n + '('\n + '\\\\[(\\\\d+|\\\\*|\".*\"|\\'.*\\')\\\\]' // followed by same\n + '|' // OR\n + '\\\\.[a-z$_][a-z$_\\\\d]*' // dot, then JS variable syntax\n + '|' // OR\n + '\\\\.\\\\*' // dot, then wildcard star\n + ')*' // (both) may repeat any number of times\n + '$'\n , 'i'\n);\n\nconst { re } = utils;\nconst ERR_INVALID = 'Invalid glob notation: ';\n\n/**\n * `Notation.Glob` is a utility for validating, comparing and sorting\n * dot-notation globs.\n *\n * You can use {@link http://www.linfo.org/wildcard.html|wildcard} stars `*`\n * and negate the notation by prepending a bang `!`. A star will include all\n * the properties at that level and a negated notation will be excluded.\n * @name Glob\n * @memberof Notation\n * @class\n *\n * @example\n * // for the following object;\n * { name: 'John', billing: { account: { id: 1, active: true } } };\n *\n * 'billing.account.*' // represents value `{ id: 1, active: true }`\n * 'billing.account.id' // represents value `1`\n * '!billing.account.*' // represents value `{ name: 'John' }`\n * 'name' // represents `'John'`\n * '*' // represents the whole object\n *\n * @example\n * var glob = new Notation.Glob('billing.account.*');\n * glob.test('billing.account.id'); // true\n */\nclass Glob {\n\n /**\n * Constructs a `Notation.Glob` object with the given glob string.\n * @constructs Notation.Glob\n * @param {String} glob - Notation string with globs.\n *\n * @throws {NotationError} - If given notation glob is invalid.\n */\n constructor(glob) {\n const ins = Glob._inspect(glob);\n const notes = Glob.split(ins.absGlob);\n this._ = {\n ...ins,\n notes,\n // below props will be set at first getter call\n parent: undefined, // don't set to null\n regexp: undefined\n };\n }\n\n // --------------------------------\n // INSTANCE PROPERTIES\n // --------------------------------\n\n /**\n * Gets the normalized glob notation string.\n * @name Notation.Glob#glob\n * @type {String}\n */\n get glob() {\n return this._.glob;\n }\n\n /**\n * Gets the absolute glob notation without the negation prefix `!` and\n * redundant trailing wildcards.\n * @name Notation.Glob#absGlob\n * @type {String}\n */\n get absGlob() {\n return this._.absGlob;\n }\n\n /**\n * Specifies whether this glob is negated with a `!` prefix.\n * @name Notation.Glob#isNegated\n * @type {Boolean}\n */\n get isNegated() {\n return this._.isNegated;\n }\n\n /**\n * Represents this glob in regular expressions.\n * Note that the negation prefix (`!`) is ignored, if any.\n * @name Notation.Glob#regexp\n * @type {RegExp}\n */\n get regexp() {\n // setting on first call instead of in constructor, for performance\n // optimization.\n this._.regexp = this._.regexp || Glob.toRegExp(this.absGlob);\n return this._.regexp;\n }\n\n /**\n * List of notes (levels) of this glob notation. Note that trailing,\n * redundant wildcards are removed from the original glob notation.\n * @name Notation.Glob#notes\n * @alias Notation.Glob#levels\n * @type {Array}\n */\n get notes() {\n return this._.notes;\n }\n\n /**\n * Alias of `Notation.Glob#notes`.\n * @private\n * @name Notation.Glob#notes\n * @alias Notation.Glob#levels\n * @type {Array}\n */\n get levels() {\n return this._.notes;\n }\n\n /**\n * Gets the first note of this glob notation.\n * @name Notation.Glob#first\n * @type {String}\n */\n get first() {\n return this.notes[0];\n }\n\n /**\n * Gets the last note of this glob notation.\n * @name Notation.Glob#last\n * @type {String}\n */\n get last() {\n return this.notes[this.notes.length - 1];\n }\n\n /**\n * Gets the parent notation (up to but excluding the last note) from the\n * glob notation string. Note that initially, trailing/redundant wildcards\n * are removed.\n * @name Notation.Glob#parent\n * @type {String}\n *\n * @example\n * const glob = Notation.Glob.create;\n * glob('first.second.*').parent; // \"first.second\"\n * glob('*.x.*').parent; // \"*\" (\"*.x.*\" normalizes to \"*.x\")\n * glob('*').parent; // null (no parent)\n */\n get parent() {\n // setting on first call instead of in constructor, for performance\n // optimization.\n if (this._.parent === undefined) {\n this._.parent = this.notes.length > 1\n ? this.absGlob.slice(0, -this.last.length).replace(/\\.$/, '')\n : null;\n }\n return this._.parent;\n }\n\n // --------------------------------\n // INSTANCE METHODS\n // --------------------------------\n\n /**\n * Checks whether the given notation value matches the source notation\n * glob.\n * @name Notation.Glob#test\n * @function\n * @param {String} notation - The notation string to be tested. Cannot have\n * any globs.\n * @returns {Boolean} -\n * @throws {NotationError} - If given `notation` is not valid or contains\n * any globs.\n *\n * @example\n * const glob = new Notation.Glob('!prop.*.name');\n * glob.test(\"prop.account.name\"); // true\n */\n test(notation) {\n if (!Notation.isValid(notation)) {\n throw new NotationError(`Invalid notation: '${notation}'`);\n }\n // return this.regexp.test(notation);\n return Glob._covers(this, notation);\n }\n\n /**\n * Specifies whether this glob notation can represent (or cover) the given\n * glob notation. Note that negation prefix is ignored, if any.\n * @name Notation.Glob#covers\n * @function\n *\n * @param {String|Array|Glob} glob Glob notation string, glob\n * notes array or a `Notation.Glob` instance.\n * @returns {Boolean} -\n *\n * @example\n * const glob = Notation.Glob.create;\n * glob('*.y').covers('x.y') // true\n * glob('x[*].y').covers('x[*]') // false\n */\n covers(glob) {\n return Glob._covers(this, glob);\n }\n\n /**\n * Gets the intersection of this and the given glob notations. When\n * restrictive, if any one of them is negated, the outcome is negated.\n * Otherwise, only if both of them are negated, the outcome is negated.\n * @name Notation.Glob#intersect\n * @function\n *\n * @param {String} glob - Second glob to be used.\n * @param {Boolean} [restrictive=false] - Whether the intersection should\n * be negated when one of the globs is negated.\n * @returns {String} - Intersection notation if any; otherwise `null`.\n *\n * @example\n * const glob = Notation.Glob.create;\n * glob('x.*').intersect('!*.y') // 'x.y'\n * glob('x.*').intersect('!*.y', true) // '!x.y'\n */\n intersect(glob, restrictive = false) {\n return Glob._intersect(this.glob, glob, restrictive);\n }\n\n // --------------------------------\n // STATIC MEMBERS\n // --------------------------------\n\n /**\n * Basically constructs a new `Notation.Glob` instance\n * with the given glob string.\n * @name Notation.Glob.create\n * @function\n *\n * @param {String} glob - The source notation glob.\n * @returns {Glob} -\n *\n * @example\n * const glob = Notation.Glob.create(strGlob);\n * // equivalent to:\n * const glob = new Notation.Glob(strGlob);\n */\n static create(glob) {\n return new Glob(glob);\n }\n\n // Created test at: https://regex101.com/r/tJ7yI9/4\n /**\n * Validates the given notation glob.\n * @name Notation.Glob.isValid\n * @function\n *\n * @param {String} glob - Notation glob to be validated.\n * @returns {Boolean} -\n */\n static isValid(glob) {\n return typeof glob === 'string' && reVALIDATOR.test(glob);\n }\n\n /**\n * Specifies whether the given glob notation includes any valid wildcards\n * (`*`) or negation bang prefix (`!`).\n * @name Notation.Glob.hasMagic\n * @function\n *\n * @param {String} glob - Glob notation to be checked.\n * @returns {Boolean} -\n */\n static hasMagic(glob) {\n return Glob.isValid(glob) && (re.WILDCARDS.test(glob) || glob[0] === '!');\n }\n\n /**\n * Gets a regular expressions instance from the given glob notation.\n * Note that the bang `!` prefix will be ignored if the given glob is negated.\n * @name Notation.Glob.toRegExp\n * @function\n *\n * @param {String} glob - Glob notation to be converted.\n *\n * @returns {RegExp} - A `RegExp` instance from the glob.\n *\n * @throws {NotationError} - If given notation glob is invalid.\n */\n static toRegExp(glob) {\n if (!Glob.isValid(glob)) {\n throw new NotationError(`${ERR_INVALID} '${glob}'`);\n }\n\n let g = glob.indexOf('!') === 0 ? glob.slice(1) : glob;\n g = utils.pregQuote(g)\n // `[*]` always represents array index e.g. `[1]`. so we'd replace\n // `\\[\\*\\]` with `\\[\\d+\\]` but we should also watch for quotes: e.g.\n // `[\"x[*]y\"]`\n .replace(/\\\\\\[\\\\\\*\\\\\\](?=(?:[^\"]|\"[^\"]*\")*$)(?=(?:[^']|'[^']*')*$)/g, '\\\\[\\\\d+\\\\]')\n // `*` within quotes (e.g. ['*']) is non-wildcard, just a regular star char.\n // `*` outside of quotes is always JS variable syntax e.g. `prop.*`\n .replace(/\\\\\\*(?=(?:[^\"]|\"[^\"]*\")*$)(?=(?:[^']|'[^']*')*$)/g, '[a-z$_][a-z$_\\\\d]*')\n .replace(/\\\\\\?/g, '.');\n return new RegExp('^' + g + '(?:[\\\\[\\\\.].+|$)', 'i');\n // it should either end ($) or continue with a dot or bracket. So for\n // example, `company.*` will produce `/^company\\.[a-z$_][a-z$_\\\\d]*(?:[\\\\[|\\\\.].+|$)/`\n // which will match both `company.name` and `company.address.street` but\n // will not match `some.company.name`. Also `!password` will not match\n // `!password_reset`.\n }\n\n /**\n * Specifies whether first glob notation can represent (or cover) the\n * second.\n * @name Notation.Glob._covers\n * @function\n * @private\n *\n * @param {String|Object|Glob} globA Source glob notation string\n * or inspection result object or `Notation.Glob` instance.\n * @param {String|Object|Glob} globB Glob notation string or\n * inspection result object or `Notation.Glob` instance.\n * @param {Boolean} [match=false] Check whether notes match instead of\n * `globA` covers `globB`.\n * @returns {Boolean} -\n *\n * @example\n * const { covers } = Notation.Glob;\n * covers('*.y', 'x.y') // true\n * covers('x.y', '*.y') // false\n * covers('x.y', '*.y', true) // true\n * covers('x[*].y', 'x[*]') // false\n */\n static _covers(globA, globB, match = false) {\n const a = typeof globA === 'string'\n ? new Glob(globA)\n : globA; // assume (globA instanceof Notation.Glob || utils.type(globA) === 'object')\n\n const b = typeof globB === 'string'\n ? new Glob(globB)\n : globB;\n\n const notesA = a.notes || Glob.split(a.absGlob);\n const notesB = b.notes || Glob.split(b.absGlob);\n\n if (!match) {\n // !x.*.* does not cover !x.* or x.* bec. !x.*.* ≠ x.* ≠ x\n // x.*.* covers x.* bec. x.*.* = x.* = x\n if (a.isNegated && notesA.length > notesB.length) return false;\n }\n\n let covers = true;\n const fn = match ? _matchesNote : _coversNote;\n for (let i = 0; i < notesA.length; i++) {\n if (!fn(notesA[i], notesB[i])) {\n covers = false;\n break;\n }\n }\n return covers;\n }\n\n /**\n * Gets the intersection notation of two glob notations. When restrictive,\n * if any one of them is negated, the outcome is negated. Otherwise, only\n * if both of them are negated, the outcome is negated.\n * @name Notation.Glob._intersect\n * @function\n * @private\n *\n * @param {String} globA - First glob to be used.\n * @param {String} globB - Second glob to be used.\n * @param {Boolean} [restrictive=false] - Whether the intersection should\n * be negated when one of the globs is negated.\n * @returns {String} - Intersection notation if any; otherwise `null`.\n * @example\n * _intersect('!*.y', 'x.*', false) // 'x.y'\n * _intersect('!*.y', 'x.*', true) // '!x.y'\n */\n static _intersect(globA, globB, restrictive = false) {\n // const bang = restrictive\n // ? (globA[0] === '!' || globB[0] === '!' ? '!' : '')\n // : (globA[0] === '!' && globB[0] === '!' ? '!' : '');\n\n const notesA = Glob.split(globA, true);\n const notesB = Glob.split(globB, true);\n\n let bang;\n if (restrictive) {\n bang = globA[0] === '!' || globB[0] === '!' ? '!' : '';\n } else {\n if (globA[0] === '!' && globB[0] === '!') {\n bang = '!';\n } else {\n bang = ((notesA.length > notesB.length && globA[0] === '!')\n || (notesB.length > notesA.length && globB[0] === '!'))\n ? '!'\n : '';\n }\n }\n\n const len = Math.max(notesA.length, notesB.length);\n let notesI = [];\n let a, b;\n // x.* ∩ *.y » x.y\n // x.*.* ∩ *.y » x.y.*\n // x.*.z ∩ *.y » x.y.z\n // x.y ∩ *.b » (n/a)\n // x.y ∩ a.* » (n/a)\n for (let i = 0; i < len; i++) {\n a = notesA[i];\n b = notesB[i];\n if (a === b) {\n notesI.push(a);\n } else if (a && re.WILDCARD.test(a)) {\n if (!b) {\n notesI.push(a);\n } else {\n notesI.push(b);\n }\n } else if (b && re.WILDCARD.test(b)) {\n if (!a) {\n notesI.push(b);\n } else {\n notesI.push(a);\n }\n } else if (a && !b) {\n notesI.push(a);\n } else if (!a && b) {\n notesI.push(b);\n } else { // if (a !== b) {\n notesI = [];\n break;\n }\n }\n\n if (notesI.length > 0) return bang + utils.joinNotes(notesI);\n return null;\n }\n\n /**\n * Undocumented.\n * @name Notation.Glob._inspect\n * @function\n * @private\n *\n * @param {String} glob -\n * @returns {Object} -\n */\n static _inspect(glob) {\n let g = glob.trim();\n if (!Glob.isValid(g)) {\n throw new NotationError(`${ERR_INVALID} '${glob}'`);\n }\n const isNegated = g[0] === '!';\n // trailing wildcards are only redundant if not negated\n if (!isNegated) g = utils.removeTrailingWildcards(g);\n const absGlob = isNegated ? g.slice(1) : g;\n return {\n glob: g,\n absGlob,\n isNegated,\n // e.g. [*] or [1] are array globs. [\"1\"] is not.\n isArrayGlob: (/^\\[[^'\"]/).test(absGlob)\n };\n }\n\n /**\n * Splits the given glob notation string into its notes (levels). Note that\n * this will exclude the `!` negation prefix, if it exists.\n * @name Notation.Glob.split\n * @function\n *\n * @param {String} glob Glob notation string to be splitted.\n * @param {String} [normalize=false] Whether to remove trailing, redundant\n * wildcards.\n * @returns {Array} - A string array of glob notes (levels).\n * @throws {NotationError} - If given glob notation is invalid.\n *\n * @example\n * Notation.Glob.split('*.list[2].prop') // ['*', 'list', '[2]', 'prop']\n * // you can get the same result from the .notes property of a Notation.Glob instance.\n */\n static split(glob, normalize = false) {\n if (!Glob.isValid(glob)) {\n throw new NotationError(`${ERR_INVALID} '${glob}'`);\n }\n const neg = glob[0] === '!';\n // trailing wildcards are redundant only when not negated\n const g = !neg && normalize ? utils.removeTrailingWildcards(glob) : glob;\n return g.replace(/^!/, '').match(reMATCHER);\n }\n\n /**\n * Compares two given notation globs and returns an integer value as a\n * result. This is generally used to sort glob arrays. Loose globs (with\n * stars especially closer to beginning of the glob string) and globs\n * representing the parent/root of the compared property glob come first.\n * Verbose/detailed/exact globs come last. (`* < *.abc < abc`).\n *\n * For instance; `store.address` comes before `store.address.street`. So\n * this works both for `*, store.address.street, !store.address` and `*,\n * store.address, !store.address.street`. For cases such as `prop.id` vs\n * `!prop.id` which represent the same property; the negated glob comes\n * last.\n * @name Notation.Glob.compare\n * @function\n *\n * @param {String} globA - First notation glob to be compared.\n * @param {String} globB - Second notation glob to be compared.\n *\n * @returns {Number} - Returns `-1` if `globA` comes first, `1` if `globB`\n * comes first and `0` if equivalent priority.\n *\n * @throws {NotationError} - If either `globA` or `globB` is invalid glob\n * notation.\n *\n * @example\n * const { compare } = Notation.Glob;\n * compare('*', 'info.user') // -1\n * compare('*', '[*]') // 0\n * compare('info.*.name', 'info.user') // 1\n */\n static compare(globA, globB) {\n // trivial case, both are exactly the same!\n // or both are wildcard e.g. `*` or `[*]`\n if (globA === globB || (re.WILDCARD.test(globA) && re.WILDCARD.test(globB))) return 0;\n\n const a = new Glob(globA);\n const b = new Glob(globB);\n\n // Check depth (number of levels)\n if (a.notes.length === b.notes.length) {\n // check and compare if these are globs that represent items in the\n // \"same\" array. if not, this will return 0.\n const aIdxCompare = _compareArrayItemGlobs(a, b);\n // we'll only continue comparing if 0 is returned\n if (aIdxCompare !== 0) return aIdxCompare;\n\n // count wildcards\n const wildCountA = (a.absGlob.match(re.WILDCARDS) || []).length;\n const wildCountB = (b.absGlob.match(re.WILDCARDS) || []).length;\n if (wildCountA === wildCountB) {\n // check for negation\n if (!a.isNegated && b.isNegated) return -1;\n if (a.isNegated && !b.isNegated) return 1;\n // both are negated or neither are, return alphabetical\n return a.absGlob < b.absGlob ? -1 : (a.absGlob > b.absGlob ? 1 : 0);\n }\n return wildCountA > wildCountB ? -1 : 1;\n }\n\n return a.notes.length < b.notes.length ? -1 : 1;\n }\n\n /**\n * Sorts the notation globs in the given array by their priorities. Loose\n * globs (with stars especially closer to beginning of the glob string);\n * globs representing the parent/root of the compared property glob come\n * first. Verbose/detailed/exact globs come last. (`* < *.y < x.y`).\n *\n * For instance; `store.address` comes before `store.address.street`. For\n * cases such as `prop.id` vs `!prop.id` which represent the same property;\n * the negated glob wins (comes last).\n * @name Notation.Glob.sort\n * @function\n *\n * @param {Array} globList - The notation globs array to be sorted. The\n * passed array reference is modified.\n * @returns {Array} - Logically sorted globs array.\n *\n * @example\n * Notation.Glob.sort(['!prop.*.name', 'prop.*', 'prop.id']) // ['prop.*', 'prop.id', '!prop.*.name'];\n */\n static sort(globList) {\n return globList.sort(Glob.compare);\n }\n\n /**\n * Normalizes the given notation globs array by removing duplicate or\n * redundant items, eliminating extra verbosity (also with intersection\n * globs) and returns a priority-sorted globs array.\n *\n *
    \n *
  • If any exact duplicates found, all except first is removed.\n *
    example: `['car', 'dog', 'car']` normalizes to `['car', 'dog']`.
  • \n *
  • If both normal and negated versions of a glob are found, negated wins.\n *
    example: `['*', 'id', '!id']` normalizes to `['*', '!id']`.
  • \n *
  • If a glob is covered by another, it's removed.\n *
    example: `['car.*', 'car.model']` normalizes to `['car']`.
  • \n *
  • If a negated glob is covered by another glob, it's kept.\n *
    example: `['*', 'car', '!car.model']` normalizes as is.
  • \n *
  • If a negated glob is not covered by another or it does not cover any other;\n * then we check for for intersection glob. If found, adds them to list;\n * removes the original negated.\n *
    example: `['car.*', '!*.model']` normalizes as to `['car', '!car.model']`.
  • \n *
  • In restrictive mode; if a glob is covered by another negated glob, it's removed.\n * Otherwise, it's kept.\n *
    example: `['*', '!car.*', 'car.model']` normalizes to `['*', '!car']` if restrictive.
  • \n *
\n * @name Notation.Glob.normalize\n * @function\n *\n * @param {Array} globList - Notation globs array to be normalized.\n * @param {Boolean} [restrictive=false] - Whether negated items strictly\n * remove every match. Note that, regardless of this option, if any item has an\n * exact negated version; non-negated is always removed.\n * @returns {Array} -\n *\n * @throws {NotationError} - If any item in globs list is invalid.\n *\n * @example\n * const { normalize } = Notation.Glob;\n * normalize(['*', '!id', 'name', '!car.model', 'car.*', 'id', 'name']); // ['*', '!id', '!car.model']\n * normalize(['!*.id', 'user.*', 'company']); // ['company', 'user', '!company.id', '!user.id']\n * normalize(['*', 'car.model', '!car.*']); // [\"*\", \"!car.*\", \"car.model\"]\n * // restrictive normalize:\n * normalize(['*', 'car.model', '!car.*'], true); // [\"*\", \"!car.*\"]\n */\n static normalize(globList, restrictive = false) {\n const { _inspect, _covers, _intersect } = Glob;\n\n const original = utils.ensureArray(globList);\n if (original.length === 0) return [];\n\n const list = original\n // prevent mutation\n .concat()\n // move negated globs to top so that we inspect non-negated globs\n // against others first. when complete, we'll sort with our\n // .compare() function.\n .sort(restrictive ? _negFirstSort : _negLastSort)\n // turning string array into inspect-obj array, so that we'll not\n // run _inspect multiple times in the inner loop. this also\n // pre-validates each glob.\n .map(_inspect);\n\n // early return if we have a single item\n if (list.length === 1) {\n const g = list[0];\n // single negated item is redundant\n if (g.isNegated) return [];\n // return normalized\n return [g.glob];\n }\n\n // flag to return an empty array (in restrictive mode), if true.\n let negateAll = false;\n\n // we'll push keepers in this array\n let normalized = [];\n // we'll need to remember excluded globs, so that we can move to next\n // item early.\n const ignored = {};\n\n // storage to keep intersections.\n // using an object to prevent duplicates.\n let intersections = {};\n\n function checkAddIntersection(gA, gB) {\n const inter = _intersect(gA, gB, restrictive);\n if (!inter) return;\n // if the intersection result has an inverted version in the\n // original list, don't add this.\n const hasInverted = restrictive ? false : original.indexOf(_invert(inter)) >= 0;\n // also if intersection result is in the current list, don't add it.\n if (list.indexOf(inter) >= 0 || hasInverted) return;\n intersections[inter] = inter;\n }\n\n // iterate each glob by comparing it to remaining globs.\n utils.eachRight(list, (a, indexA) => {\n\n // if `strict` is enabled, return empty if a negate-all is found\n // (which itself is also redundant if single): '!*' or '![*]'\n if (re.NEGATE_ALL.test(a.glob)) {\n negateAll = true;\n if (restrictive) return false;\n }\n\n // flags\n let duplicate = false;\n let hasExactNeg = false;\n // flags for negated\n let negCoversPos = false;\n let negCoveredByPos = false;\n let negCoveredByNeg = false;\n // flags for non-negated (positive)\n let posCoversPos = false;\n let posCoveredByNeg = false;\n let posCoveredByPos = false;\n\n utils.eachRight(list, (b, indexB) => {\n // don't inspect glob with itself\n if (indexA === indexB) return; // move to next\n // console.log(indexA, a.glob, 'vs', b.glob);\n\n if (a.isArrayGlob !== b.isArrayGlob) {\n throw new NotationError(`Integrity failed. Cannot have both object and array notations for root level: ${JSON.stringify(original)}`);\n }\n\n // remove if duplicate\n if (a.glob === b.glob) {\n list.splice(indexA, 1);\n duplicate = true;\n return false; // break out\n }\n\n // remove if positive has an exact negated (negated wins when\n // normalized) e.g. ['*', 'a', '!a'] => ['*', '!a']\n if (!a.isNegated && _isReverseOf(a, b)) {\n // list.splice(indexA, 1);\n ignored[a.glob] = true;\n hasExactNeg = true;\n return false; // break out\n }\n\n // if already excluded b, go on to next\n if (ignored[b.glob]) return; // next\n\n const coversB = _covers(a, b);\n const coveredByB = coversB ? false : _covers(b, a);\n if (a.isNegated) {\n if (b.isNegated) {\n // if negated (a) covered by any other negated (b); remove (a)!\n if (coveredByB) {\n negCoveredByNeg = true;\n // list.splice(indexA, 1);\n ignored[a.glob] = true;\n return false; // break out\n }\n } else {\n /* istanbul ignore if */\n if (coversB) negCoversPos = true;\n if (coveredByB) negCoveredByPos = true;\n // try intersection if none covers the other and only\n // one of them is negated.\n if (!coversB && !coveredByB) {\n checkAddIntersection(a.glob, b.glob);\n }\n }\n } else {\n if (b.isNegated) {\n // if positive (a) covered by any negated (b); remove (a)!\n if (coveredByB) {\n posCoveredByNeg = true;\n if (restrictive) {\n // list.splice(indexA, 1);\n ignored[a.glob] = true;\n return false; // break out\n }\n return; // next\n }\n // try intersection if none covers the other and only\n // one of them is negated.\n if (!coversB && !coveredByB) {\n checkAddIntersection(a.glob, b.glob);\n }\n } else {\n if (coversB) posCoversPos = coversB;\n // if positive (a) covered by any other positive (b); remove (a)!\n if (coveredByB) {\n posCoveredByPos = true;\n if (restrictive) {\n // list.splice(indexA, 1);\n return false; // break out\n }\n }\n }\n }\n\n });\n\n // const keepNeg = (negCoversPos || negCoveredByPos) && !negCoveredByNeg;\n const keepNeg = restrictive\n ? (negCoversPos || negCoveredByPos) && negCoveredByNeg === false\n : negCoveredByPos && negCoveredByNeg === false;\n const keepPos = restrictive\n ? (posCoversPos || posCoveredByPos === false) && posCoveredByNeg === false\n : posCoveredByNeg || posCoveredByPos === false;\n const keep = duplicate === false\n && hasExactNeg === false\n && (a.isNegated ? keepNeg : keepPos);\n\n if (keep) {\n normalized.push(a.glob);\n } else {\n // this is excluded from final (normalized) list, so mark as\n // ignored (don't remove from \"list\" for now)\n ignored[a.glob] = true;\n }\n });\n\n if (restrictive && negateAll) return [];\n\n intersections = Object.keys(intersections);\n if (intersections.length > 0) {\n // merge normalized list with intersections if any\n normalized = normalized.concat(intersections);\n // we have new (intersection) items, so re-normalize\n return Glob.normalize(normalized, restrictive);\n }\n\n return Glob.sort(normalized);\n }\n\n /**\n * Undocumented. See `.union()`\n * @name Notation.Glob._compareUnion\n * @function\n * @private\n *\n * @param {Array} globsListA -\n * @param {Array} globsListB -\n * @param {Boolean} restrictive -\n * @param {Array} union -\n * @returns {Array} -\n */\n static _compareUnion(globsListA, globsListB, restrictive, union = []) {\n const { _covers } = Glob;\n\n const { _inspect, _intersect } = Glob;\n\n utils.eachRight(globsListA, globA => {\n if (union.indexOf(globA) >= 0) return; // next\n\n const a = _inspect(globA);\n\n // if wildcard only, add...\n if (re.WILDCARD.test(a.absGlob)) {\n union.push(a.glob); // push normalized glob\n return; // next\n }\n\n let notCovered = false;\n let hasExact = false;\n let negCoversNeg = false;\n let posCoversNeg = false;\n let posCoversPos = false;\n let negCoversPos = false;\n\n const intersections = [];\n\n utils.eachRight(globsListB, globB => {\n\n // keep if has exact in the other\n if (globA === globB) hasExact = true;\n\n const b = _inspect(globB);\n\n // keep negated if:\n // 1) any negated covers it\n // 2) no positive covers it\n // keep positive if:\n // 1) no positive covers it OR any negated covers it\n\n notCovered = !_covers(b, a);\n if (notCovered) {\n if (a.isNegated && b.isNegated) {\n const inter = _intersect(a.glob, b.glob, restrictive);\n if (inter && union.indexOf(inter) === -1) intersections.push(inter);\n }\n return; // next\n }\n\n if (a.isNegated) {\n if (b.isNegated) {\n negCoversNeg = !hasExact;\n } else {\n posCoversNeg = true; // set flag\n }\n } else {\n if (!b.isNegated) {\n posCoversPos = !hasExact;\n } else {\n negCoversPos = true; // set flag\n }\n }\n\n });\n\n\n const keep = a.isNegated\n ? (!posCoversNeg || negCoversNeg)\n : (!posCoversPos || negCoversPos);\n\n if (hasExact || keep || (notCovered && !a.isNegated)) {\n union.push(a.glob); // push normalized glob\n return;\n }\n\n if (a.isNegated && posCoversNeg && !negCoversNeg && intersections.length > 0) {\n union = union.concat(intersections);\n }\n\n });\n\n return union;\n }\n\n /**\n * Gets the union from the given couple of glob arrays and returns a new\n * array of globs.\n *
    \n *
  • If the exact same element is found in both\n * arrays, one of them is removed to prevent duplicates.\n *
    example: `['!id', 'name'] ∪ ['!id']` unites to `['!id', 'name']`
  • \n *
  • If any non-negated item is covered by a glob in the same\n * or other array, the redundant item is removed.\n *
    example: `['*', 'name'] ∪ ['email']` unites to `['*']`
  • \n *
  • If one of the arrays contains a negated equivalent of an\n * item in the other array, the negated item is removed.\n *
    example: `['!id'] ∪ ['id']` unites to `['id']`
  • \n *
  • If any item covers/matches a negated item in the other array,\n * the negated item is removed.\n *
    example #1: `['!user.id'] ∪ ['user.*']` unites to `['user']`\n *
    example #2: `['*'] ∪ ['!password']` unites to `['*']`\n *
  • \n *
\n * @name Notation.Glob.union\n * @function\n *\n * @param {Array} globsA - First array of glob strings.\n * @param {Array} globsB - Second array of glob strings.\n * @param {Boolean} [restrictive=false] - Whether negated items in each of\n * the lists, strictly remove every match in themselves (not the cross\n * list). This option is used when pre-normalizing each glob list and\n * normalizing the final union list.\n *\n * @returns {Array} -\n *\n * @example\n * const a = ['user.*', '!user.email', 'car.model', '!*.id'];\n * const b = ['!*.date', 'user.email', 'car', '*.age'];\n * const { union } = Notation.Glob;\n * union(a, b) // ['car', 'user', '*.age', '!car.date', '!user.id']\n */\n static union(globsA, globsB, restrictive) {\n const { normalize, _compareUnion } = Glob;\n\n const listA = normalize(globsA, restrictive);\n const listB = normalize(globsB, restrictive);\n\n if (listA.length === 0) return listB;\n if (listB.length === 0) return listA;\n\n // TODO: below should be optimized\n let union = _compareUnion(listA, listB, restrictive);\n union = _compareUnion(listB, listA, restrictive, union);\n return normalize(union, restrictive);\n }\n\n}\n\n// --------------------------------\n// HELPERS\n// --------------------------------\n\n// used by static _covers\nfunction _coversNote(a, b) {\n if (!a || !b) return false; // glob e.g.: [2] does not cover [2][1]\n const bIsArr = re.ARRAY_GLOB_NOTE.test(b);\n // obj-wildcard a will cover b if not array\n if (a === '*') return !bIsArr;\n // arr-wildcard a will cover b if array\n if (a === '[*]') return bIsArr;\n // seems, a is not wildcard so,\n // if b is wildcard (obj or arr) won't be covered\n if (re.WILDCARD.test(b)) return false;\n // normalize both and check for equality\n // e.g. x.y and x['y'] are the same\n return utils.normalizeNote(a) === utils.normalizeNote(b);\n}\n// function _coversNote(a, b) {\n// if (!a || !b) return false; // glob e.g.: [2] does not cover [2][1]\n// a = utils.normalizeNote(a, true);\n// b = utils.normalizeNote(b, true);\n// if (a === b) return true;\n// const bIsArr = re.ARRAY_GLOB_NOTE.test(b);\n// return (a === '*' && !bIsArr) || (a === '[*]' && bIsArr);\n// }\n// used by static _covers\nfunction _matchesNote(a, b) {\n if (!a || !b) return true; // glob e.g.: [2][1] matches [2] and vice-versa.\n return _coversNote(a, b) || _coversNote(b, a);\n}\n\n// used by _compareArrayItemGlobs() for getting a numeric index from array note.\n// we'll use these indexes to sort higher to lower, as removing order; to\n// prevent shifted indexes.\nfunction _idxVal(note) {\n // we return -1 for wildcard bec. we need it to come last\n\n // below will never execute when called from _compareArrayItemGlobs\n /* istanbul ignore next */\n // if (note === '[*]') return -1;\n\n // e.g. '[2]' » 2\n return parseInt(note.replace(/[[\\]]/, ''), 10);\n}\n\nfunction _compArrIdx(lastA, lastB) {\n const iA = _idxVal(lastA);\n const iB = _idxVal(lastB);\n\n // below will never execute when called from _compareArrayItemGlobs\n /* istanbul ignore next */\n // if (iA === iB) return 0;\n\n return iA > iB ? -1 : 1;\n}\n\n// when we remove items from an array (via e.g. filtering), we first need to\n// remove the item with the greater index so indexes of other items (that are to\n// be removed from the same array) do not shift. so below is for comparing 2\n// globs if they represent 2 items from the same array.\n\n// example items from same array: ![*][2] ![0][*] ![0][1] ![0][3]\n// should be sorted as ![0][3] ![*][2] ![0][1] ![0][*]\nfunction _compareArrayItemGlobs(a, b) {\n const reANote = re.ARRAY_GLOB_NOTE;\n // both should be negated\n if (!a.isNegated\n || !b.isNegated\n // should be same length (since we're comparing for items in same\n // array)\n || a.notes.length !== b.notes.length\n // last notes should be array brackets\n || !reANote.test(a.last)\n || !reANote.test(b.last)\n // last notes should be different to compare\n || a.last === b.last\n ) return 0;\n\n // negated !..[*] should come last\n if (a.last === '[*]') return 1; // b is first\n if (b.last === '[*]') return -1; // a is first\n\n if (a.parent && b.parent) {\n const { _covers } = Glob;\n if (_covers(a.parent, b.parent, true)) {\n return _compArrIdx(a.last, b.last);\n }\n return 0;\n }\n return _compArrIdx(a.last, b.last);\n}\n\n// x vs !x.*.* » false\n// x vs !x[*] » true\n// x[*] vs !x » true\n// x[*] vs !x[*] » false\n// x.* vs !x.* » false\nfunction _isReverseOf(a, b) {\n return a.isNegated !== b.isNegated\n && a.absGlob === b.absGlob;\n}\n\nfunction _invert(glob) {\n return glob[0] === '!' ? glob.slice(1) : '!' + glob;\n}\n\nconst _rx = /^\\s*!/;\nfunction _negFirstSort(a, b) {\n const negA = _rx.test(a);\n const negB = _rx.test(b);\n if (negA && negB) return a.length >= b.length ? 1 : -1;\n if (negA) return -1;\n if (negB) return 1;\n return 0;\n}\nfunction _negLastSort(a, b) {\n const negA = _rx.test(a);\n const negB = _rx.test(b);\n if (negA && negB) return a.length >= b.length ? 1 : -1;\n if (negA) return 1;\n if (negB) return -1;\n return 0;\n}\n\n// --------------------------------\n// EXPORT\n// --------------------------------\n\nexport { Glob };\n","/* eslint no-use-before-define:0, consistent-return:0, max-statements:0, max-len:0 */\n\nimport { Glob } from './notation.glob';\nimport { NotationError } from './notation.error';\nimport { utils } from '../utils';\n\nconst ERR = {\n SOURCE: 'Invalid source. Expected a data object or array.',\n DEST: 'Invalid destination. Expected a data object or array.',\n NOTATION: 'Invalid notation: ',\n NOTA_OBJ: 'Invalid notations object. ',\n NO_INDEX: 'Implied index does not exist: ',\n NO_PROP: 'Implied property does not exist: '\n};\n\n// created test @ https://regex101.com/r/vLE16M/2\nconst reMATCHER = /(\\[(\\d+|\".*\"|'.*'|`.*`)\\]|[a-z$_][a-z$_\\d]*)/gi;\n// created test @ https://regex101.com/r/fL3PJt/1/\n// /^([a-z$_][a-z$_\\d]*|\\[(\\d+|\".*\"|'.*'|`.*`)\\])(\\[(\\d+|\".*\"|'.*'|`.*`)\\]|(\\.[a-z$_][a-z$_\\d]*))*$/i\nconst reVALIDATOR = new RegExp(\n '^('\n + '[a-z$_][a-z$_\\\\d]*' // JS variable syntax\n + '|' // OR\n + '\\\\[(\\\\d+|\".*\"|\\'.*\\')\\\\]' // array index or object bracket notation\n + ')' // exactly once\n + '('\n + '\\\\[(\\\\d+|\".*\"|\\'.*\\')\\\\]' // followed by same\n + '|' // OR\n + '\\\\.[a-z$_][a-z$_\\\\d]*' // dot, then JS variable syntax\n + ')*' // (both) may repeat any number of times\n + '$'\n , 'i'\n);\n\nconst DEFAULT_OPTS = Object.freeze({\n strict: false,\n preserveIndices: false\n});\n\n/**\n * Notation.js for Node and Browser.\n *\n * Like in most programming languages, JavaScript makes use of dot-notation to\n * access the value of a member of an object (or class). `Notation` class\n * provides various methods for modifying / processing the contents of the\n * given object; by parsing object notation strings or globs.\n *\n * Note that this class will only deal with enumerable properties of the source\n * object; so it should be used to manipulate data objects. It will not deal\n * with preserving the prototype-chain of the given object.\n *\n * @author Onur Yıldırım \n * @license MIT\n */\nclass Notation {\n\n /**\n * Initializes a new instance of `Notation`.\n *\n * @param {Object|Array} [source={}] - The source object (or array) to be\n * notated. Can either be an array or object. If omitted, defaults to an\n * empty object.\n * @param {Object} [options] - Notation options.\n * @param {Boolean} [options.strict=false] - Whether to throw either when\n * a notation path does not exist on the source (i.e. `#get()` and `#remove()`\n * methods); or notation path exists but overwriting is disabled (i.e.\n * `#set()` method). (Note that `.inspectGet()` and `.inspectRemove()` methods\n * are exceptions). It's recommended to set this to `true` and prevent silent\n * failures if you're working with sensitive data. Regardless of `strict` option,\n * it will always throw on invalid notation syntax or other crucial failures.\n *\n * @example\n * const obj = { car: { brand: \"Dodge\", model: \"Charger\", year: 1970 } };\n * const notation = new Notation(obj);\n * notation.get('car.model') // » \"Charger\"\n * notation.remove('car.model').set('car.color', 'red').value\n * // » { car: { brand: \"Dodge\", year: 1970, color: \"red\" } }\n */\n constructor(source, options) {\n if (arguments.length === 0) {\n this._source = {};\n } else if (!utils.isCollection(source)) {\n throw new NotationError(ERR.SOURCE);\n } else {\n this._source = source;\n }\n\n this._isArray = utils.type(this._source) === 'array';\n this.options = options;\n }\n\n // --------------------------------\n // INSTANCE PROPERTIES\n // --------------------------------\n\n /**\n * Gets or sets notation options.\n * @type {Object}\n */\n get options() {\n return this._options;\n }\n\n set options(value) {\n this._options = {\n ...DEFAULT_OPTS,\n ...(this._options || {}),\n ...(value || {})\n };\n }\n\n /**\n * Gets the value of the source object.\n * @type {Object|Array}\n *\n * @example\n * const person = { name: \"Onur\" };\n * const me = Notation.create(person)\n * .set(\"age\", 36)\n * .set(\"car.brand\", \"Ford\")\n * .set(\"car.model\", \"Mustang\")\n * .value;\n * console.log(me); // { name: \"Onur\", age: 36, car: { brand: \"Ford\", model: \"Mustang\" } }\n * console.log(person === me); // true\n */\n get value() {\n return this._source;\n }\n\n // --------------------------------\n // INSTANCE METHODS\n // --------------------------------\n\n /**\n * Recursively iterates through each key of the source object and invokes\n * the given callback function with parameters, on each non-object value.\n *\n * @param {Function} callback - The callback function to be invoked on\n * each on each non-object value. To break out of the loop, return `false`\n * from within the callback.\n * Callback signature: `callback(notation, key, value, object) { ... }`\n *\n * @returns {Notation} - The current `Notation` instance (self).\n *\n * @example\n * const obj = { car: { brand: \"Dodge\", model: \"Charger\", year: 1970 } };\n * Notation.create(obj).each(function (notation, key, value, object) {\n * console.log(notation, value);\n * });\n * // \"car.brand\" \"Dodge\"\n * // \"car.model\" \"Charger\"\n * // \"car.year\" 1970\n */\n each(callback) {\n _each(this._source, callback);\n return this;\n }\n\n /**\n * Iterates through each note of the given notation string by evaluating\n * it on the source object.\n *\n * @param {String} notation - The notation string to be iterated through.\n * @param {Function} callback - The callback function to be invoked on\n * each iteration. To break out of the loop, return `false` from within\n * the callback. Signature: `callback(levelValue, note, index, list)`\n *\n * @returns {Notation} - The current `Notation` instance (self).\n *\n * @example\n * const obj = { car: { brand: \"Dodge\", model: \"Charger\", year: 1970 } };\n * Notation.create(obj)\n * .eachValue(\"car.brand\", function (levelValue, note, index, list) {\n * console.log(note, levelValue); // \"car.brand\" \"Dodge\"\n * });\n */\n eachValue(notation, callback) {\n let level = this._source;\n Notation.eachNote(notation, (levelNotation, note, index, list) => {\n level = utils.hasOwn(level, note) ? level[note] : undefined;\n if (callback(level, levelNotation, note, index, list) === false) return false;\n\n });\n return this;\n }\n\n /**\n * Gets the list of notations from the source object (keys).\n *\n * @returns {Array} - An array of notation strings.\n *\n * @example\n * const obj = { car: { brand: \"Dodge\", model: \"Charger\", year: 1970 } };\n * const notations = Notation.create(obj).getNotations();\n * console.log(notations); // [ \"car.brand\", \"car.model\", \"car.year\" ]\n */\n getNotations() {\n const list = [];\n this.each(notation => {\n list.push(notation);\n });\n return list;\n }\n\n /**\n * Deeply clones the source object. This is also useful if you want to\n * prevent mutating the original source object.\n *\n *
\n * Note that `Notation` expects a data object (or array) with enumerable\n * properties. In addition to plain objects and arrays; supported cloneable\n * property/value types are primitives (such as `String`, `Number`,\n * `Boolean`, `Symbol`, `null` and `undefined`) and built-in types (such as\n * `Date` and `RegExp`).\n *\n * Enumerable properties with types other than these (such as methods,\n * special objects, custom class instances, etc) will be copied by reference.\n * Non-enumerable properties will not be cloned.\n *\n * If you still need full clone support, you can use a library like lodash.\n * e.g. `Notation.create(_.cloneDeep(source))`\n *
\n *\n * @returns {Notation} - The current `Notation` instance (self).\n *\n * @example\n * const mutated = Notation.create(source1).set('newProp', true).value;\n * console.log(source1.newProp); // ——» true\n *\n * const cloned = Notation.create(source2).clone().set('newProp', true).value;\n * console.log('newProp' in source2); // ——» false\n * console.log(cloned.newProp); // ——» true\n */\n clone() {\n this._source = utils.cloneDeep(this._source);\n return this;\n }\n\n /**\n * Flattens the source object to a single-level object with notated keys.\n *\n * @returns {Notation} - The current `Notation` instance (self).\n *\n * @example\n * const obj = { car: { brand: \"Dodge\", model: \"Charger\", year: 1970 } };\n * console.log(Notation.create(obj).flatten().value);\n * // {\n * // \"car.brand\": \"Dodge\",\n * // \"car.model\": \"Charger\",\n * // \"car.year\": 1970\n * // }\n */\n flatten() {\n const o = {};\n this.each((notation, key, value) => {\n o[notation] = value;\n });\n this._source = o;\n return this;\n }\n\n /**\n * Aggregates notated keys of a (single-level) object, and nests them under\n * their corresponding properties. This is the opposite of `Notation#flatten`\n * method. This might be useful when expanding a flat object fetched from\n * a database.\n * @alias Notation#aggregate\n * @chainable\n *\n * @returns {Notation} - The current `Notation` instance (self).\n *\n * @example\n * const obj = { \"car.brand\": \"Dodge\", \"car.model\": \"Charger\", \"car.year\": 1970 }\n * const expanded = Notation.create(obj).expand().value;\n * console.log(expanded); // { car: { brand: \"Dodge\", model: \"Charger\", year: 1970 } };\n */\n expand() {\n this._source = Notation.create({}).merge(this._source).value;\n return this;\n }\n\n /**\n * Alias for `#expand`\n * @private\n * @returns {Notation} -\n */\n aggregate() {\n return this.expand();\n }\n\n /**\n * Inspects the given notation on the source object by checking\n * if the source object actually has the notated property;\n * and getting its value if exists.\n * @param {String} notation - The notation string to be inspected.\n * @returns {InspectResult} - The result object.\n *\n * @example\n * Notation.create({ car: { year: 1970 } }).inspectGet(\"car.year\");\n * // { has: true, value: 1970, lastNote: 'year', lastNoteNormalized: 'year' }\n * Notation.create({ car: { year: 1970 } }).inspectGet(\"car.color\");\n * // { has: false }\n * Notation.create({ car: { color: undefined } }).inspectGet(\"car.color\");\n * // { has: true, value: undefined, lastNote: 'color', lastNoteNormalized: 'color' }\n * Notation.create({ car: { brands: ['Ford', 'Dodge'] } }).inspectGet(\"car.brands[1]\");\n * // { has: true, value: 'Dodge', lastNote: '[1]', lastNoteNormalized: 1 }\n */\n inspectGet(notation) {\n let level = this._source;\n let result = { has: false, value: undefined };\n let parent;\n Notation.eachNote(notation, (levelNotation, note, index) => {\n const lastNoteNormalized = utils.normalizeNote(note);\n if (utils.hasOwn(level, lastNoteNormalized)) {\n level = level[lastNoteNormalized];\n parent = level;\n result = {\n notation,\n has: true,\n value: level,\n type: utils.type(level),\n level: index + 1,\n lastNote: note,\n lastNoteNormalized\n };\n } else {\n // level = undefined;\n result = {\n notation,\n has: false,\n type: 'undefined',\n level: index + 1,\n lastNote: note,\n lastNoteNormalized\n };\n return false; // break out\n }\n });\n\n if (parent === undefined || (result.has && parent === result.value)) parent = this._source;\n result.parentIsArray = utils.type(parent) === 'array';\n\n return result;\n }\n\n /**\n * Notation inspection result object.\n * @typedef Notation~InspectResult\n * @type Object\n * @property {String} notation - Notation that is inspected.\n * @property {Boolean} has - Indicates whether the source object has the\n * given notation as a (leveled) enumerable property. If the property\n * exists but has a value of `undefined`, this will still return `true`.\n * @property {*} value - The value of the notated property. If the source\n * object does not have the notation, the value will be `undefined`.\n * @property {String} type - The type of the notated property. If the source\n * object does not have the notation, the type will be `\"undefined\"`.\n * @property {String} lastNote - Last note of the notation, if actually\n * exists. For example, last note of `'a.b.c'` is `'c'`.\n * @property {String|Number} lastNoteNormalized - Normalized representation\n * of the last note of the notation, if actually exists. For example, last\n * note of `'a.b[1]` is `'[1]'` and will be normalized to number `1`; which\n * indicates an array index.\n * @property {Boolean} parentIsArray - Whether the parent object of the\n * notation path is an array.\n */\n\n /**\n * Inspects and removes the given notation from the source object by\n * checking if the source object actually has the notated property; and\n * getting its value if exists, before removing the property.\n *\n * @param {String} notation - The notation string to be inspected.\n *\n * @returns {InspectResult} - The result object.\n *\n * @example\n * const obj = { name: \"John\", car: { year: 1970 } };\n * let result = Notation.create(obj).inspectRemove(\"car.year\");\n * // result » { notation: \"car.year\", has: true, value: 1970, lastNote: \"year\", lastNoteNormalized: \"year\" }\n * // obj » { name: \"John\", car: {} }\n *\n * result = Notation.create({ car: { year: 1970 } }).inspectRemove(\"car.color\");\n * // result » { notation: \"car.color\", has: false }\n * Notation.create({ car: { color: undefined } }).inspectRemove(\"car['color']\");\n * // { notation: \"car.color\", has: true, value: undefined, lastNote: \"['color']\", lastNoteNormalized: \"color\" }\n *\n * const obj = { car: { colors: [\"black\", \"white\"] } };\n * const result = Notation.create().inspectRemove(\"car.colors[0]\");\n * // result » { notation: \"car.colors[0]\", has: true, value: \"black\", lastNote: \"[0]\", lastNoteNormalized: 0 }\n * // obj » { car: { colors: [(empty), \"white\"] } }\n */\n inspectRemove(notation) {\n if (!notation) throw new Error(ERR.NOTATION + `'${notation}'`);\n const parentNotation = Notation.parent(notation);\n const parent = parentNotation ? this.get(parentNotation, null) : this._source;\n const parentIsArray = utils.type(parent) === 'array';\n const notes = Notation.split(notation);\n const lastNote = notes[notes.length - 1];\n const lastNoteNormalized = utils.normalizeNote(lastNote);\n\n let result, value;\n if (utils.hasOwn(parent, lastNoteNormalized)) {\n value = parent[lastNoteNormalized];\n result = {\n notation,\n has: true,\n value,\n type: utils.type(value),\n level: notes.length,\n lastNote,\n lastNoteNormalized,\n parentIsArray\n };\n\n // if `preserveIndices` is enabled and this is an array, we'll\n // splice the item out. otherwise, we'll use `delete` syntax to\n // empty the item.\n if (!this.options.preserveIndices && parentIsArray) {\n parent.splice(lastNoteNormalized, 1);\n } else {\n delete parent[lastNoteNormalized];\n }\n } else {\n result = {\n notation,\n has: false,\n type: 'undefined',\n level: notes.length,\n lastNote,\n lastNoteNormalized,\n parentIsArray\n };\n }\n\n return result;\n }\n\n /**\n * Checks whether the source object has the given notation\n * as a (leveled) enumerable property. If the property exists\n * but has a value of `undefined`, this will still return `true`.\n * @param {String} notation - The notation string to be checked.\n * @returns {Boolean} -\n *\n * @example\n * Notation.create({ car: { year: 1970 } }).has(\"car.year\"); // true\n * Notation.create({ car: { year: undefined } }).has(\"car.year\"); // true\n * Notation.create({}).has(\"car.color\"); // false\n */\n has(notation) {\n return this.inspectGet(notation).has;\n }\n\n /**\n * Checks whether the source object has the given notation\n * as a (leveled) defined enumerable property. If the property\n * exists but has a value of `undefined`, this will return `false`.\n * @param {String} notation - The notation string to be checked.\n * @returns {Boolean} -\n *\n * @example\n * Notation.create({ car: { year: 1970 } }).hasDefined(\"car.year\"); // true\n * Notation.create({ car: { year: undefined } }).hasDefined(\"car.year\"); // false\n * Notation.create({}).hasDefined(\"car.color\"); // false\n */\n hasDefined(notation) {\n return this.inspectGet(notation).value !== undefined;\n }\n\n /**\n * Gets the value of the corresponding property at the given notation.\n *\n * @param {String} notation - The notation string to be processed.\n * @param {String} [defaultValue] - The default value to be returned if the\n * property is not found or enumerable.\n *\n * @returns {*} - The value of the notated property.\n * @throws {NotationError} - If `strict` option is enabled, `defaultValue`\n * is not set and notation does not exist.\n *\n * @example\n * Notation.create({ car: { brand: \"Dodge\" } }).get(\"car.brand\"); // \"Dodge\"\n * Notation.create({ car: {} }).get(\"car.model\", \"Challenger\"); // \"Challenger\"\n * Notation.create({ car: { model: undefined } }).get(\"car.model\", \"Challenger\"); // undefined\n *\n * @example get value when strict option is enabled\n * // strict option defaults to false\n * Notation.create({ car: {} }).get(\"car.model\"); // undefined\n * Notation.create({ car: {} }, { strict: false }).get(\"car.model\"); // undefined\n * // below will throw bec. strict = true, car.model does not exist\n * // and no default value is given.\n * Notation.create({ car: {} }, { strict: true }).get(\"car.model\");\n */\n get(notation, defaultValue) {\n const result = this.inspectGet(notation);\n // if strict and no default value is set, check if implied index or prop\n // exists\n if (this.options.strict && arguments.length < 2 && !result.has) {\n const msg = result.parentIsArray ? ERR.NO_INDEX : ERR.NO_PROP;\n throw new NotationError(msg + `'${notation}'`);\n }\n return result.has ? result.value : defaultValue;\n }\n\n /**\n * Sets the value of the corresponding property at the given notation. If\n * the property does not exist, it will be created and nested at the\n * calculated level. If it exists; its value will be overwritten by\n * default.\n * @chainable\n *\n * @param {String} notation - The notation string to be processed.\n * @param {*} value - The value to be set for the notated property.\n * @param {String|Boolean} [mode=\"overwrite\"] - Write mode. By default,\n * this is set to `\"overwrite\"` which sets the value by overwriting the\n * target object property or array item at index. To insert an array item\n * (by shifting the index, instead of overwriting); set to `\"insert\"`. To\n * prevent overwriting the value if exists, explicitly set to `false`.\n *\n * @returns {Notation} - The current `Notation` instance (self).\n *\n * @throws {NotationError} - If strict notation is enabled, `overwrite`\n * option is set to `false` and attempted to overwrite an existing value.\n *\n * @example\n * const obj = { car: { brand: \"Dodge\", year: 1970 } };\n * Notation.create(obj)\n * .set(\"car.brand\", \"Ford\")\n * .set(\"car.model\", \"Mustang\")\n * .set(\"car.year\", 1965, false)\n * .set(\"car.color\", \"red\")\n * .set(\"boat\", \"none\");\n * console.log(obj);\n * // { notebook: \"Mac\", car: { brand: \"Ford\", model: \"Mustang\", year: 1970, color: \"red\" }, boat: \"none\" };\n */\n set(notation, value, mode = 'overwrite') {\n if (!notation.trim()) throw new NotationError(ERR.NOTATION + `'${notation}'`);\n if (mode === true) mode = 'overwrite';\n let level = this._source;\n let currentIsLast, nCurrentNote, nNextNote, nextIsArrayNote, type;\n const insertErrMsg = 'Cannot set value by inserting at index, on an object';\n\n Notation.eachNote(notation, (levelNotation, note, index, list) => {\n currentIsLast = index === list.length - 1;\n nCurrentNote = nNextNote || utils.normalizeNote(note);\n nNextNote = currentIsLast ? null : utils.normalizeNote(list[index + 1]);\n type = utils.type(level);\n\n if (type === 'array' && typeof nCurrentNote !== 'number') {\n const parent = Notation.parent(levelNotation) || 'source';\n throw new NotationError(`Cannot set string key '${note}' on array ${parent}`);\n }\n\n // check if the property is at this level\n if (utils.hasOwn(level, nCurrentNote, type)) {\n // check if we're at the last level\n if (currentIsLast) {\n // if mode is \"overwrite\", assign the value.\n if (mode === 'overwrite') {\n level[nCurrentNote] = value;\n } else if (mode === 'insert') {\n if (type === 'array') {\n level.splice(nCurrentNote, 0, value);\n } else {\n throw new NotationError(insertErrMsg);\n }\n }\n // otherwise, will not overwrite\n } else {\n // if not last level; just re-reference the current level.\n level = level[nCurrentNote];\n }\n } else {\n if (currentIsLast && type !== 'array' && mode === 'insert') {\n throw new NotationError(insertErrMsg);\n }\n\n // if next normalized note is a number, it indicates that the\n // current note is actually an array.\n nextIsArrayNote = typeof nNextNote === 'number';\n\n // we don't have this property at this level so; if this is the\n // last level, we set the value if not, we set an empty\n // collection for the next level\n level[nCurrentNote] = (currentIsLast ? value : (nextIsArrayNote ? [] : {}));\n level = level[nCurrentNote];\n }\n });\n return this;\n }\n\n /**\n * Just like the `.set()` method but instead of a single notation\n * string, an object of notations and values can be passed.\n * Sets the value of each corresponding property at the given\n * notation. If a property does not exist, it will be created\n * and nested at the calculated level. If it exists; its value\n * will be overwritten by default.\n * @chainable\n *\n * @param {Object} notationsObject - The notations object to be processed.\n * This can either be a regular object with non-dotted keys\n * (which will be merged to the first/root level of the source object);\n * or a flattened object with notated (dotted) keys.\n * @param {Boolean} [overwrite=true] - Whether to overwrite a property if\n * exists.\n *\n * @returns {Notation} - The current `Notation` instance (self).\n *\n * @example\n * const obj = { car: { brand: \"Dodge\", year: 1970 } };\n * Notation.create(obj).merge({\n * \"car.brand\": \"Ford\",\n * \"car.model\": \"Mustang\",\n * \"car.year\": 1965,\n * \"car.color\": \"red\",\n * \"boat\": \"none\"\n * });\n * console.log(obj);\n * // { car: { brand: \"Ford\", model: \"Mustang\", year: 1970, color: \"red\" }, boat: \"none\" };\n */\n merge(notationsObject, overwrite = true) {\n if (utils.type(notationsObject) !== 'object') {\n throw new NotationError(ERR.NOTA_OBJ + 'Expected an object.');\n }\n let value;\n utils.each(Object.keys(notationsObject), notation => {\n value = notationsObject[notation];\n this.set(notation, value, overwrite);\n });\n return this;\n }\n\n /**\n * Removes the properties by the given list of notations from the source\n * object and returns a new object with the removed properties.\n * Opposite of `merge()` method.\n *\n * @param {Array} notations - The notations array to be processed.\n *\n * @returns {Object} - An object with the removed properties.\n *\n * @example\n * const obj = { car: { brand: \"Dodge\", year: 1970 }, notebook: \"Mac\" };\n * const separated = Notation.create(obj).separate([\"car.brand\", \"boat\" ]);\n * console.log(separated);\n * // { notebook: \"Mac\", car: { brand: \"Ford\" } };\n * console.log(obj);\n * // { car: { year: 1970 } };\n */\n separate(notations) {\n if (utils.type(notations) !== 'array') {\n throw new NotationError(ERR.NOTA_OBJ + 'Expected an array.');\n }\n const o = new Notation({});\n utils.each(notations, notation => {\n const result = this.inspectRemove(notation);\n o.set(notation, result.value);\n });\n this._source = o._source;\n return this;\n }\n\n /**\n * Deep clones the source object while filtering its properties by the\n * given glob notations. Includes all matched properties and removes\n * the rest.\n *\n * The difference between regular notations and glob-notations is that;\n * with the latter, you can use wildcard stars (*) and negate the notation\n * by prepending a bang (!). A negated notation will be excluded.\n *\n * Order of the globs does not matter; they will be logically sorted. Loose\n * globs will be processed first and verbose globs or normal notations will\n * be processed last. e.g. `[ \"car.model\", \"*\", \"!car.*\" ]` will be\n * normalized and sorted as `[ \"*\", \"!car\" ]`.\n *\n * Passing no parameters or passing a glob of `\"!*\"` or `[\"!*\"]` will empty\n * the source object. See `Notation.Glob` class for more information.\n * @chainable\n *\n * @param {Array|String} globList - Glob notation list to be processed.\n * @param {Object} [options] - Filter options.\n * @param {Boolean} [options.restrictive=false] - Whether negated items\n * strictly remove every match. Note that, regardless of this option, if\n * any item has an exact negated version; non-negated is always removed.\n *\n * @returns {Notation} - The current `Notation` instance (self). To get the\n * filtered value, call `.value` property on the instance.\n *\n * @example\n * const car = { brand: \"Ford\", model: { name: \"Mustang\", year: 1970 } };\n * const n = Notation.create(car);\n *\n * console.log(n.filter([ \"*\", \"!model.year\" ]).value); // { brand: \"Ford\", model: { name: \"Mustang\" } }\n * console.log(n.filter(\"model.name\").value); // { model: { name: \"Mustang\" } }\n * console.log(car); // { brand: \"Ford\", model: { name: \"Mustang\", year: 1970 } }\n * console.log(n.filter().value); // {} // —» equivalent to n.filter(\"\") or n.filter(\"!*\")\n */\n filter(globList, options = {}) {\n const { re } = utils;\n\n // ensure array, normalize and sort the globs in logical order. this\n // also concats the array first (to prevent mutating the original\n // array).\n const globs = Glob.normalize(globList, options.restrictive);\n const len = globs.length;\n const empty = this._isArray ? [] : {};\n\n // if globs is \"\" or [\"\"] or [\"!*\"] or [\"![*]\"] set source to empty and return.\n if (len === 0 || (len === 1 && (!globs[0] || re.NEGATE_ALL.test(globs[0])))) {\n this._source = empty;\n return this;\n }\n\n const cloned = utils.cloneDeep(this.value);\n\n const firstIsWildcard = re.WILDCARD.test(globs[0]);\n // if globs only consist of \"*\" or \"[*]\"; set the \"clone\" as source and\n // return.\n if (len === 1 && firstIsWildcard) {\n this._source = cloned;\n return this;\n }\n\n let filtered;\n // if the first item of sorted globs is \"*\" or \"[*]\" we set the source\n // to the (full) \"copy\" and remove the wildcard from globs (not to\n // re-process).\n if (firstIsWildcard) {\n filtered = new Notation(cloned);\n globs.shift();\n } else {\n // otherwise we set an empty object or array as the source so that\n // we can add notations/properties to it.\n filtered = new Notation(empty);\n }\n\n // iterate through globs\n utils.each(globs, globNotation => {\n // console.log('globNotation', globNotation);\n const g = new Glob(globNotation);\n const { glob, absGlob, isNegated, levels } = g;\n let normalized, emptyValue, eType;\n // check whether the glob ends with `.*` or `[*]` then remove\n // trailing glob note and decide for empty value (if negated). for\n // non-negated, trailing wildcards are already removed by\n // normalization.\n if (absGlob.slice(-2) === '.*') {\n normalized = absGlob.slice(0, -2);\n /* istanbul ignore else */\n if (isNegated) emptyValue = {};\n eType = 'object';\n } else if (absGlob.slice(-3) === '[*]') {\n normalized = absGlob.slice(0, -3);\n /* istanbul ignore else */\n if (isNegated) emptyValue = [];\n eType = 'array';\n } else {\n normalized = absGlob;\n }\n\n // we'll check glob vs value integrity if emptyValue is set; and throw if needed.\n const errGlobIntegrity = `Integrity failed for glob '${glob}'. Cannot set empty ${eType} for '${normalized}' which has a type of `; // ...\n\n // check if remaining normalized glob has no wildcard stars e.g.\n // \"a.b\" or \"!a.b.c\" etc..\n if (re.WILDCARDS.test(normalized) === false) {\n if (isNegated) {\n // inspect and directly remove the notation if negated.\n // we need the inspection for the detailed error below.\n const insRemove = filtered.inspectRemove(normalized);\n // console.log('insRemove', insRemove);\n\n // if original glob had `.*` at the end, it means remove\n // contents (not itself). so we'll set an empty object.\n // meaning `some.prop` (prop) is removed completely but\n // `some.prop.*` (prop) results in `{}`. For array notation\n // (`[*]`), we'll set an empty array.\n if (emptyValue) {\n // e.g. for glob `![0].x.*` we expect to set `[0].x = {}`\n // but if `.x` is not an object (or array), we should fail.\n const vType = insRemove.type;\n const errMsg = errGlobIntegrity + `'${vType}'.`;\n // in non-strict mode, only exceptions are `null` and\n // `undefined`, for which we won't throw but we'll not\n // set an empty obj/arr either.\n\n const isValSet = utils.isset(insRemove.value);\n // on critical type mismatch we throw\n // or if original value is undefined or null in strict mode we throw\n if ((isValSet && vType !== eType) || (!isValSet && this.options.strict)) {\n throw new NotationError(errMsg);\n }\n // if parent is an array, we'll insert the value at\n // index bec. we've removed the item and indexes are\n // shifted. Otherwise, we'll simply overwrite the\n // object property value.\n const setMode = insRemove.parentIsArray ? 'insert' : 'overwrite';\n // console.log('setting', normalized, emptyValue, setMode);\n filtered.set(normalized, emptyValue, setMode);\n }\n } else {\n // directly set the same notation from the original\n const insGet = this.inspectGet(normalized); // Notation.create(original).inspectGet ...\n /* istanbul ignore else */\n if (insGet.has) filtered.set(normalized, insGet.value, 'overwrite');\n }\n // move to the next\n return true;\n }\n\n // if glob has wildcard(s), we'll iterate through keys of the source\n // object and see if (full) notation of each key matches the current\n // glob.\n\n // important! we will iterate with eachRight to prevent shifted\n // indexes when removing items from arrays.\n const reverseIterateIfArray = true;\n\n _each(this._source, (originalNotation, key, value) => {\n const originalIsCovered = Glob.create(normalized).covers(originalNotation);\n // console.log('» normalized:', normalized, 'covers', originalNotation, '»', originalIsCovered);\n if (!originalIsCovered) return true; // break\n\n if (this.options.strict && emptyValue) {\n // since original is covered and we have emptyValue set (due\n // to trailing wildcard), here we'll check value vs glob\n // integrity; (only if we're in strict mode).\n\n const vType = utils.type(value);\n // types and number of levels are the same?\n if (vType !== eType\n // we subtract 1 from number of levels bec. the last\n // note is removed since we have emptyValue set.\n && Notation.split(originalNotation).length === levels.length - 1) {\n throw new NotationError(errGlobIntegrity + `'${vType}'.`);\n }\n }\n\n // iterating each note of original notation. i.e.:\n // note1.note2.note3 is iterated from left to right, as:\n // 'note1', 'note1.note2', 'note1.note2.note3' — in order.\n Notation.eachNote(originalNotation, levelNotation => {\n // console.log(' level »', glob, 'covers', levelNotation, '»', g.test(levelNotation));\n\n if (g.test(levelNotation)) {\n const levelLen = Notation.split(levelNotation).length;\n /* istanbul ignore else */\n if (isNegated && levels.length <= levelLen) {\n // console.log(' » removing', levelNotation, 'of', originalNotation);\n filtered.remove(levelNotation);\n // we break and return early if removed bec. e.g.\n // when 'note1.note2' (parent) of\n // 'note1.note2.note3' is also removed, we no more\n // have 'note3'.\n return false;\n }\n // console.log(' » setting', levelNotation, '=', value);\n filtered.set(levelNotation, value, 'overwrite');\n }\n });\n }, reverseIterateIfArray);\n });\n // finally set the filtered's value as the source of our instance and\n // return.\n this._source = filtered.value;\n return this;\n }\n\n /**\n * Removes the property from the source object, at the given notation.\n * @alias Notation#delete\n * @chainable\n * @param {String} notation - The notation to be inspected.\n * @returns {Notation} - The current `Notation` instance (self).\n * @throws {NotationError} - If `strict` option is enabled and notation\n * does not exist.\n *\n * @example\n * const obj = { notebook: \"Mac\", car: { model: \"Mustang\" } };\n * Notation.create(obj).remove(\"car.model\");\n * console.log(obj); // { notebook: \"Mac\", car: { } }\n */\n remove(notation) {\n const result = this.inspectRemove(notation);\n // if strict, check if implied index or prop exists\n if (this.options.strict && !result.has) {\n const msg = result.parentIsArray ? ERR.NO_INDEX : ERR.NO_PROP;\n throw new NotationError(msg + `'${notation}'`);\n }\n return this;\n }\n\n /**\n * Alias of `Notation#remove`\n * @private\n * @param {String} notation -\n * @returns {Notation} -\n */\n delete(notation) {\n this.remove(notation);\n return this;\n }\n\n /**\n * Copies the notated property from the source collection and adds it to the\n * destination — only if the source object actually has that property.\n * This is different than a property with a value of `undefined`.\n * @chainable\n *\n * @param {Object|Array} destination - The destination object that the notated\n * properties will be copied to.\n * @param {String} notation - The notation to get the corresponding property\n * from the source object.\n * @param {String} [newNotation=null] - The notation to set the source property\n * on the destination object. In other words, the copied property will be\n * renamed to this value before set on the destination object. If not set,\n * `notation` argument will be used.\n * @param {Boolean} [overwrite=true] - Whether to overwrite the property on\n * the destination object if it exists.\n *\n * @returns {Notation} - The current `Notation` instance (self).\n *\n * @throws {NotationError} - If `destination` is not a valid collection.\n * @throws {NotationError} - If `notation` or `newNotation` is invalid.\n *\n * @example\n * const obj = { car: { brand: \"Ford\", model: \"Mustang\" } };\n * const models = { dodge: \"Charger\" };\n * Notation.create(obj).copyTo(models, \"car.model\", \"ford\");\n * console.log(models);\n * // { dodge: \"Charger\", ford: \"Mustang\" }\n * // source object (obj) is not modified\n */\n copyTo(destination, notation, newNotation = null, overwrite = true) {\n if (!utils.isCollection(destination)) throw new NotationError(ERR.DEST);\n const result = this.inspectGet(notation);\n if (result.has) {\n const newN = utils.getNewNotation(newNotation, notation);\n Notation.create(destination).set(newN, result.value, overwrite);\n }\n return this;\n }\n\n /**\n * Copies the notated property from the target collection and adds it to\n * (own) source object — only if the target object actually has that\n * property. This is different than a property with a value of `undefined`.\n * @chainable\n *\n * @param {Object|Array} target - The target collection that the notated\n * properties will be copied from.\n * @param {String} notation - The notation to get the corresponding\n * property from the target object.\n * @param {String} [newNotation=null] - The notation to set the copied\n * property on our source collection. In other words, the copied property\n * will be renamed to this value before set. If not set, `notation`\n * argument will be used.\n * @param {Boolean} [overwrite=true] - Whether to overwrite the property on\n * our collection if it exists.\n *\n * @returns {Notation} - The current `Notation` instance (self).\n *\n * @throws {NotationError} - If `target` is not a valid collection.\n * @throws {NotationError} - If `notation` or `newNotation` is invalid.\n *\n * @example\n * const obj = { car: { brand: \"Ford\", model: \"Mustang\" } };\n * const models = { dodge: \"Charger\" };\n * Notation.create(obj).copyFrom(models, \"dodge\", \"car.model\", true);\n * console.log(obj);\n * // { car: { brand: \"Ford\", model: \"Charger\" } }\n * // models object is not modified\n */\n copyFrom(target, notation, newNotation = null, overwrite = true) {\n if (!utils.isCollection(target)) throw new NotationError(ERR.DEST);\n const result = Notation.create(target).inspectGet(notation);\n if (result.has) {\n const newN = utils.getNewNotation(newNotation, notation);\n this.set(newN, result.value, overwrite);\n }\n return this;\n }\n\n /**\n * Removes the notated property from the source (own) collection and adds\n * it to the destination — only if the source collection actually has that\n * property. This is different than a property with a value of `undefined`.\n * @chainable\n *\n * @param {Object|Array} destination - The destination collection that the\n * notated properties will be moved to.\n * @param {String} notation - The notation to get the corresponding\n * property from the source object.\n * @param {String} [newNotation=null] - The notation to set the source\n * property on the destination object. In other words, the moved property\n * will be renamed to this value before set on the destination object. If\n * not set, `notation` argument will be used.\n * @param {Boolean} [overwrite=true] - Whether to overwrite the property on\n * the destination object if it exists.\n *\n * @returns {Notation} - The current `Notation` instance (self).\n *\n * @throws {NotationError} - If `destination` is not a valid collection.\n * @throws {NotationError} - If `notation` or `newNotation` is invalid.\n *\n * @example\n * const obj = { car: { brand: \"Ford\", model: \"Mustang\" } };\n * const models = { dodge: \"Charger\" };\n * Notation.create(obj).moveTo(models, \"car.model\", \"ford\");\n * console.log(obj);\n * // { car: { brand: \"Ford\" } }\n * console.log(models);\n * // { dodge: \"Charger\", ford: \"Mustang\" }\n */\n moveTo(destination, notation, newNotation = null, overwrite = true) {\n if (!utils.isCollection(destination)) throw new NotationError(ERR.DEST);\n const result = this.inspectRemove(notation);\n if (result.has) {\n const newN = utils.getNewNotation(newNotation, notation);\n Notation.create(destination).set(newN, result.value, overwrite);\n }\n return this;\n }\n\n /**\n * Removes the notated property from the target collection and adds it to (own)\n * source collection — only if the target object actually has that property.\n * This is different than a property with a value of `undefined`.\n * @chainable\n *\n * @param {Object|Array} target - The target collection that the notated\n * properties will be moved from.\n * @param {String} notation - The notation to get the corresponding property\n * from the target object.\n * @param {String} [newNotation=null] - The notation to set the target\n * property on the source object. In other words, the moved property\n * will be renamed to this value before set on the source object.\n * If not set, `notation` argument will be used.\n * @param {Boolean} [overwrite=true] - Whether to overwrite the property on\n * the source object if it exists.\n *\n * @returns {Notation} - The current `Notation` instance (self).\n *\n * @throws {NotationError} - If `target` is not a valid collection.\n * @throws {NotationError} - If `notation` or `newNotation` is invalid.\n *\n * @example\n * const obj = { car: { brand: \"Ford\", model: \"Mustang\" } };\n * const models = { dodge: \"Charger\" };\n * Notation.create(obj).moveFrom(models, \"dodge\", \"car.model\", true);\n * console.log(obj);\n * // { car: { brand: \"Ford\", model: \"Charger\" } }\n * console.log(models);\n * // {}\n */\n moveFrom(target, notation, newNotation = null, overwrite = true) {\n if (!utils.isCollection(target)) throw new NotationError(ERR.DEST);\n const result = Notation.create(target).inspectRemove(notation);\n if (result.has) {\n const newN = utils.getNewNotation(newNotation, notation);\n this.set(newN, result.value, overwrite);\n }\n return this;\n }\n\n /**\n * Renames the notated property of the source collection by the new notation.\n * @alias Notation#renote\n * @chainable\n *\n * @param {String} notation - The notation to get the corresponding\n * property (value) from the source collection.\n * @param {String} newNotation - The new notation for the targeted\n * property value. If not set, the source collection will not be modified.\n * @param {Boolean} [overwrite=true] - Whether to overwrite the property at\n * the new notation, if it exists.\n *\n * @returns {Notation} - The current `Notation` instance (self).\n *\n * @throws {NotationError} - If `notation` or `newNotation` is invalid.\n *\n * @example\n * const obj = { car: { brand: \"Ford\", model: \"Mustang\" } };\n * Notation.create(obj)\n * .rename(\"car.brand\", \"carBrand\")\n * .rename(\"car.model\", \"carModel\");\n * console.log(obj);\n * // { carBrand: \"Ford\", carModel: \"Mustang\" }\n */\n rename(notation, newNotation, overwrite) {\n return this.moveTo(this._source, notation, newNotation, overwrite);\n }\n\n /**\n * Alias for `#rename`\n * @private\n * @param {String} notation -\n * @param {String} newNotation -\n * @param {Boolean} [overwrite=true] -\n * @returns {Notation} -\n */\n renote(notation, newNotation, overwrite) {\n return this.rename(notation, newNotation, overwrite);\n }\n\n /**\n * Extracts the property at the given notation to a new object by copying\n * it from the source collection. This is equivalent to `.copyTo({},\n * notation, newNotation)`.\n * @alias Notation#copyToNew\n *\n * @param {String} notation - The notation to get the corresponding\n * property (value) from the source object.\n * @param {String} newNotation - The new notation to be set on the new\n * object for the targeted property value. If not set, `notation` argument\n * will be used.\n *\n * @returns {Object} - Returns a new object with the notated property.\n *\n * @throws {NotationError} - If `notation` or `newNotation` is invalid.\n *\n * @example\n * const obj = { car: { brand: \"Ford\", model: \"Mustang\" } };\n * const extracted = Notation.create(obj).extract(\"car.brand\", \"carBrand\");\n * console.log(extracted);\n * // { carBrand: \"Ford\" }\n * // obj is not modified\n */\n extract(notation, newNotation) {\n const o = {};\n this.copyTo(o, notation, newNotation);\n return o;\n }\n\n /**\n * Alias for `#extract`\n * @private\n * @param {String} notation -\n * @param {String} newNotation -\n * @returns {Object} -\n */\n copyToNew(notation, newNotation) {\n return this.extract(notation, newNotation);\n }\n\n /**\n * Extrudes the property at the given notation to a new collection by\n * moving it from the source collection. This is equivalent to `.moveTo({},\n * notation, newNotation)`.\n * @alias Notation#moveToNew\n *\n * @param {String} notation - The notation to get the corresponding\n * property (value) from the source object.\n * @param {String} newNotation - The new notation to be set on the new\n * object for the targeted property value. If not set, `notation` argument\n * will be used.\n *\n * @returns {Object} - Returns a new object with the notated property.\n *\n * @example\n * const obj = { car: { brand: \"Ford\", model: \"Mustang\" } };\n * const extruded = Notation.create(obj).extrude(\"car.brand\", \"carBrand\");\n * console.log(obj);\n * // { car: { model: \"Mustang\" } }\n * console.log(extruded);\n * // { carBrand: \"Ford\" }\n */\n extrude(notation, newNotation) {\n const o = {};\n this.moveTo(o, notation, newNotation);\n return o;\n }\n\n /**\n * Alias for `#extrude`\n * @private\n * @param {String} notation -\n * @param {String} newNotation -\n * @returns {Object} -\n */\n moveToNew(notation, newNotation) {\n return this.extrude(notation, newNotation);\n }\n\n // --------------------------------\n // STATIC MEMBERS\n // --------------------------------\n\n /**\n * Basically constructs a new `Notation` instance.\n * @chainable\n * @param {Object|Array} [source={}] - The source collection to be notated.\n * @param {Object} [options] - Notation options.\n * @param {Boolean} [options.strict=false] - Whether to throw when a\n * notation path does not exist on the source. (Note that `.inspectGet()`\n * and `.inspectRemove()` methods are exceptions). It's recommended to\n * set this to `true` and prevent silent failures if you're working\n * with sensitive data. Regardless of `strict` option, it will always\n * throw on invalid notation syntax or other crucial failures.\n *\n * @returns {Notation} - The created instance.\n *\n * @example\n * const obj = { car: { brand: \"Dodge\", model: \"Charger\", year: 1970 } };\n * const notation = Notation.create(obj); // equivalent to new Notation(obj)\n * notation.get('car.model') // » \"Charger\"\n * notation.remove('car.model').set('car.color', 'red').value\n * // » { car: { brand: \"Dodge\", year: 1970, color: \"red\" } }\n */\n static create(source, options) {\n if (arguments.length === 0) {\n return new Notation({});\n }\n return new Notation(source, options);\n }\n\n /**\n * Checks whether the given notation string is valid. Note that the star\n * (`*`) (which is a valid character, even if irregular) is NOT treated as\n * wildcard here. This checks for regular dot-notation, not a glob-notation.\n * For glob notation validation, use `Notation.Glob.isValid()` method. Same\n * goes for the negation character/prefix (`!`).\n *\n * @param {String} notation - The notation string to be checked.\n * @returns {Boolean} -\n *\n * @example\n * Notation.isValid('prop1.prop2.prop3'); // true\n * Notation.isValid('x'); // true\n * Notation.isValid('x.arr[0].y'); // true\n * Notation.isValid('x[\"*\"]'); // true\n * Notation.isValid('x.*'); // false (this would be valid for Notation#filter() only or Notation.Glob class)\n * Notation.isValid('@1'); // false (should be \"['@1']\")\n * Notation.isValid(null); // false\n */\n static isValid(notation) {\n return typeof notation === 'string' && reVALIDATOR.test(notation);\n }\n\n /**\n * Splits the given notation string into its notes (levels).\n * @param {String} notation Notation string to be splitted.\n * @returns {Array} - A string array of notes (levels).\n * @throws {NotationError} - If given notation is invalid.\n */\n static split(notation) {\n if (!Notation.isValid(notation)) {\n throw new NotationError(ERR.NOTATION + `'${notation}'`);\n }\n return notation.match(reMATCHER);\n }\n\n /**\n * Joins the given notes into a notation string.\n * @param {String} notes Notes (levels) to be joined.\n * @returns {String} Joined notation string.\n */\n static join(notes) {\n return utils.joinNotes(notes);\n }\n\n /**\n * Counts the number of notes/levels in the given notation.\n * @alias Notation.countLevels\n * @param {String} notation - The notation string to be processed.\n * @returns {Number} - Number of notes.\n * @throws {NotationError} - If given notation is invalid.\n */\n static countNotes(notation) {\n return Notation.split(notation).length;\n }\n\n /**\n * Alias of `Notation.countNotes`.\n * @private\n * @param {String} notation -\n * @returns {Number} -\n */\n static countLevels(notation) {\n return Notation.countNotes(notation);\n }\n\n /**\n * Gets the first (root) note of the notation string.\n * @param {String} notation - The notation string to be processed.\n * @returns {String} - First note.\n * @throws {NotationError} - If given notation is invalid.\n *\n * @example\n * Notation.first('first.prop2.last'); // \"first\"\n */\n static first(notation) {\n return Notation.split(notation)[0];\n }\n\n /**\n * Gets the last note of the notation string.\n * @param {String} notation - The notation string to be processed.\n * @returns {String} - Last note.\n * @throws {NotationError} - If given notation is invalid.\n *\n * @example\n * Notation.last('first.prop2.last'); // \"last\"\n */\n static last(notation) {\n const list = Notation.split(notation);\n return list[list.length - 1];\n }\n\n /**\n * Gets the parent notation (up to but excluding the last note)\n * from the notation string.\n * @param {String} notation - The notation string to be processed.\n * @returns {String} - Parent note if any. Otherwise, `null`.\n * @throws {NotationError} - If given notation is invalid.\n *\n * @example\n * Notation.parent('first.prop2.last'); // \"first.prop2\"\n * Notation.parent('single'); // null\n */\n static parent(notation) {\n const last = Notation.last(notation);\n return notation.slice(0, -last.length).replace(/\\.$/, '') || null;\n }\n\n /**\n * Iterates through each note/level of the given notation string.\n * @alias Notation.eachLevel\n *\n * @param {String} notation - The notation string to be iterated through.\n * @param {Function} callback - The callback function to be invoked on\n * each iteration. To break out of the loop, return `false` from within the\n * callback.\n * Callback signature: `callback(levelNotation, note, index, list) { ... }`\n *\n * @returns {void}\n * @throws {NotationError} - If given notation is invalid.\n *\n * @example\n * const notation = 'first.prop2.last';\n * Notation.eachNote(notation, function (levelNotation, note, index, list) {\n * console.log(index, note, levelNotation);\n * });\n * // 0 \"first\" \"first\"\n * // 1 \"first.prop2\" \"prop2\"\n * // 2 \"first.prop2.last\" \"last\"\n */\n static eachNote(notation, callback) {\n const notes = Notation.split(notation);\n const levelNotes = [];\n utils.each(notes, (note, index) => {\n levelNotes.push(note);\n if (callback(Notation.join(levelNotes), note, index, notes) === false) return false;\n }, Notation);\n }\n\n /**\n * Alias of `Notation.eachNote`.\n * @private\n * @param {String} notation -\n * @param {Function} callback -\n * @returns {void}\n */\n static eachLevel(notation, callback) {\n Notation.eachNote(notation, callback);\n }\n\n}\n\n/**\n * Error class specific to `Notation`.\n * @private\n *\n * @class\n * @see `{@link #Notation.Error}`\n */\nNotation.Error = NotationError;\n\n/**\n * Utility for validating, comparing and sorting dot-notation globs.\n * This is internally used by `Notation` class.\n * @private\n *\n * @class\n * @see `{@link #Notation.Glob}`\n */\nNotation.Glob = Glob;\n\n/**\n * Undocumented\n * @private\n */\nNotation.utils = utils;\n\n// --------------------------------\n// HELPERS\n// --------------------------------\n\n/**\n * Deep iterates through each note (level) of each item in the given\n * collection.\n * @private\n * @param {Object|Array} collection A data object or an array, as the source.\n * @param {Function} callback A function to be executed on each iteration,\n * with the following arguments: `(levelNotation, note, value, collection)`\n * @param {Boolean} [reverseIfArray=false] Set to `true` to iterate with\n * `eachRight` to prevent shifted indexes when removing items from arrays.\n * @param {Boolean} [byLevel=false] Indicates whether to iterate notations by\n * each level or by the end value. For example, if we have a collection of\n * `{a: { b: true } }`, and `byLevel` is set; the callback will be invoked on\n * the following notations: `a`, `a.b`. Otherwise, it will be invoked only on\n * `a.b`.\n * @param {String} [parentNotation] Storage for parent (previous) notation.\n * @param {Collection} [topSource] Storage for initial/main collection.\n * @returns {void}\n */\nfunction _each(collection, callback, reverseIfArray = false, byLevel = false, parentNotation = null, topSource = null) { // eslint-disable-line max-params\n const source = topSource || collection;\n // if (!utils.isCollection(collection)) throw ... // no need\n utils.eachItem(collection, (value, keyOrIndex) => {\n const note = typeof keyOrIndex === 'number'\n ? `[${keyOrIndex}]`\n : keyOrIndex;\n const currentNotation = Notation.join([parentNotation, note]);\n const isCollection = utils.isCollection(value);\n // if it's not a collection we'll execute the callback. if it's a\n // collection but byLevel is set, we'll also execute the callback.\n if (!isCollection || byLevel) {\n if (callback(currentNotation, note, value, source) === false) return false;\n }\n // deep iterating if collection\n if (isCollection) _each(value, callback, reverseIfArray, byLevel, currentNotation, source);\n }, null, reverseIfArray);\n}\n\n// --------------------------------\n// EXPORT\n// --------------------------------\n\nexport { Notation };\n"],"sourceRoot":""} \ No newline at end of file diff --git a/src/core/notation.error.js b/src/core/notation.error.js deleted file mode 100644 index d79c633..0000000 --- a/src/core/notation.error.js +++ /dev/null @@ -1,48 +0,0 @@ -/* eslint consistent-this:0, no-prototype-builtins:0 */ - -const setProto = Object.setPrototypeOf; - -/** - * Error class specific to `Notation`. - * @class - * @name Notation.Error - */ -class NotationError extends Error { - - /** - * Initializes a new `Notation.Error` instance. - * @hideconstructor - * @constructs Notation.Error - * @param {String} message - The error message. - */ - constructor(message = '') { - super(message); - setProto(this, NotationError.prototype); - - Object.defineProperty(this, 'name', { - enumerable: false, - writable: false, - value: 'NotationError' - }); - - Object.defineProperty(this, 'message', { - enumerable: false, - writable: true, - value: message - }); - - /* istanbul ignore else */ - if (Error.hasOwnProperty('captureStackTrace')) { // V8 - Error.captureStackTrace(this, NotationError); - } else { - Object.defineProperty(this, 'stack', { - enumerable: false, - writable: false, - value: (new Error(message)).stack - }); - } - } - -} - -export { NotationError }; diff --git a/src/core/notation.glob.js b/src/core/notation.glob.js deleted file mode 100644 index c07b521..0000000 --- a/src/core/notation.glob.js +++ /dev/null @@ -1,1118 +0,0 @@ -/* eslint no-use-before-define:0, consistent-return:0, max-statements:0 */ - -import { Notation } from './notation'; -import { NotationError } from './notation.error'; -import { utils } from '../utils'; - -// http://www.linfo.org/wildcard.html -// http://en.wikipedia.org/wiki/Glob_%28programming%29 -// http://en.wikipedia.org/wiki/Wildcard_character#Computing - -// created test @ https://regex101.com/r/U08luj/2 -const reMATCHER = /(\[(\d+|\*|".*"|'.*')\]|[a-z$_][a-z$_\d]*|\*)/gi; // ! negation should be removed first -// created test @ https://regex101.com/r/mC8unE/3 -// /^!?(\*|[a-z$_][a-z$_\d]*|\[(\d+|".*"|'.*'|`.*`|\*)\])(\[(\d+|".*"|'.*'|`.*`|\*)\]|\.[a-z$_][a-z$_\d]*|\.\*)*$/i -const reVALIDATOR = new RegExp( - '^' - + '!?(' // optional negation, only in the front - + '\\*' // wildcard star - + '|' // OR - + '[a-z$_][a-z$_\\d]*' // JS variable syntax - + '|' // OR - + '\\[(\\d+|\\*|".*"|\'.*\')\\]' // array index or wildcard, or object bracket notation - + ')' // exactly once - + '(' - + '\\[(\\d+|\\*|".*"|\'.*\')\\]' // followed by same - + '|' // OR - + '\\.[a-z$_][a-z$_\\d]*' // dot, then JS variable syntax - + '|' // OR - + '\\.\\*' // dot, then wildcard star - + ')*' // (both) may repeat any number of times - + '$' - , 'i' -); - -const { re } = utils; -const ERR_INVALID = 'Invalid glob notation: '; - -/** - * `Notation.Glob` is a utility for validating, comparing and sorting - * dot-notation globs. - * - * You can use {@link http://www.linfo.org/wildcard.html|wildcard} stars `*` - * and negate the notation by prepending a bang `!`. A star will include all - * the properties at that level and a negated notation will be excluded. - * @name Glob - * @memberof Notation - * @class - * - * @example - * // for the following object; - * { name: 'John', billing: { account: { id: 1, active: true } } }; - * - * 'billing.account.*' // represents value `{ id: 1, active: true }` - * 'billing.account.id' // represents value `1` - * '!billing.account.*' // represents value `{ name: 'John' }` - * 'name' // represents `'John'` - * '*' // represents the whole object - * - * @example - * var glob = new Notation.Glob('billing.account.*'); - * glob.test('billing.account.id'); // true - */ -class Glob { - - /** - * Constructs a `Notation.Glob` object with the given glob string. - * @constructs Notation.Glob - * @param {String} glob - Notation string with globs. - * - * @throws {NotationError} - If given notation glob is invalid. - */ - constructor(glob) { - const ins = Glob._inspect(glob); - const notes = Glob.split(ins.absGlob); - this._ = { - ...ins, - notes, - // below props will be set at first getter call - parent: undefined, // don't set to null - regexp: undefined - }; - } - - // -------------------------------- - // INSTANCE PROPERTIES - // -------------------------------- - - /** - * Gets the normalized glob notation string. - * @name Notation.Glob#glob - * @type {String} - */ - get glob() { - return this._.glob; - } - - /** - * Gets the absolute glob notation without the negation prefix `!` and - * redundant trailing wildcards. - * @name Notation.Glob#absGlob - * @type {String} - */ - get absGlob() { - return this._.absGlob; - } - - /** - * Specifies whether this glob is negated with a `!` prefix. - * @name Notation.Glob#isNegated - * @type {Boolean} - */ - get isNegated() { - return this._.isNegated; - } - - /** - * Represents this glob in regular expressions. - * Note that the negation prefix (`!`) is ignored, if any. - * @name Notation.Glob#regexp - * @type {RegExp} - */ - get regexp() { - // setting on first call instead of in constructor, for performance - // optimization. - this._.regexp = this._.regexp || Glob.toRegExp(this.absGlob); - return this._.regexp; - } - - /** - * List of notes (levels) of this glob notation. Note that trailing, - * redundant wildcards are removed from the original glob notation. - * @name Notation.Glob#notes - * @alias Notation.Glob#levels - * @type {Array} - */ - get notes() { - return this._.notes; - } - - /** - * Alias of `Notation.Glob#notes`. - * @private - * @name Notation.Glob#notes - * @alias Notation.Glob#levels - * @type {Array} - */ - get levels() { - return this._.notes; - } - - /** - * Gets the first note of this glob notation. - * @name Notation.Glob#first - * @type {String} - */ - get first() { - return this.notes[0]; - } - - /** - * Gets the last note of this glob notation. - * @name Notation.Glob#last - * @type {String} - */ - get last() { - return this.notes[this.notes.length - 1]; - } - - /** - * Gets the parent notation (up to but excluding the last note) from the - * glob notation string. Note that initially, trailing/redundant wildcards - * are removed. - * @name Notation.Glob#parent - * @type {String} - * - * @example - * const glob = Notation.Glob.create; - * glob('first.second.*').parent; // "first.second" - * glob('*.x.*').parent; // "*" ("*.x.*" normalizes to "*.x") - * glob('*').parent; // null (no parent) - */ - get parent() { - // setting on first call instead of in constructor, for performance - // optimization. - if (this._.parent === undefined) { - this._.parent = this.notes.length > 1 - ? this.absGlob.slice(0, -this.last.length).replace(/\.$/, '') - : null; - } - return this._.parent; - } - - // -------------------------------- - // INSTANCE METHODS - // -------------------------------- - - /** - * Checks whether the given notation value matches the source notation - * glob. - * @name Notation.Glob#test - * @function - * @param {String} notation - The notation string to be tested. Cannot have - * any globs. - * @returns {Boolean} - - * @throws {NotationError} - If given `notation` is not valid or contains - * any globs. - * - * @example - * const glob = new Notation.Glob('!prop.*.name'); - * glob.test("prop.account.name"); // true - */ - test(notation) { - if (!Notation.isValid(notation)) { - throw new NotationError(`Invalid notation: '${notation}'`); - } - // return this.regexp.test(notation); - return Glob._covers(this, notation); - } - - /** - * Specifies whether this glob notation can represent (or cover) the given - * glob notation. Note that negation prefix is ignored, if any. - * @name Notation.Glob#covers - * @function - * - * @param {String|Array|Glob} glob Glob notation string, glob - * notes array or a `Notation.Glob` instance. - * @returns {Boolean} - - * - * @example - * const glob = Notation.Glob.create; - * glob('*.y').covers('x.y') // true - * glob('x[*].y').covers('x[*]') // false - */ - covers(glob) { - return Glob._covers(this, glob); - } - - /** - * Gets the intersection of this and the given glob notations. When - * restrictive, if any one of them is negated, the outcome is negated. - * Otherwise, only if both of them are negated, the outcome is negated. - * @name Notation.Glob#intersect - * @function - * - * @param {String} glob - Second glob to be used. - * @param {Boolean} [restrictive=false] - Whether the intersection should - * be negated when one of the globs is negated. - * @returns {String} - Intersection notation if any; otherwise `null`. - * - * @example - * const glob = Notation.Glob.create; - * glob('x.*').intersect('!*.y') // 'x.y' - * glob('x.*').intersect('!*.y', true) // '!x.y' - */ - intersect(glob, restrictive = false) { - return Glob._intersect(this.glob, glob, restrictive); - } - - // -------------------------------- - // STATIC MEMBERS - // -------------------------------- - - /** - * Basically constructs a new `Notation.Glob` instance - * with the given glob string. - * @name Notation.Glob.create - * @function - * - * @param {String} glob - The source notation glob. - * @returns {Glob} - - * - * @example - * const glob = Notation.Glob.create(strGlob); - * // equivalent to: - * const glob = new Notation.Glob(strGlob); - */ - static create(glob) { - return new Glob(glob); - } - - // Created test at: https://regex101.com/r/tJ7yI9/4 - /** - * Validates the given notation glob. - * @name Notation.Glob.isValid - * @function - * - * @param {String} glob - Notation glob to be validated. - * @returns {Boolean} - - */ - static isValid(glob) { - return typeof glob === 'string' && reVALIDATOR.test(glob); - } - - /** - * Specifies whether the given glob notation includes any valid wildcards - * (`*`) or negation bang prefix (`!`). - * @name Notation.Glob.hasMagic - * @function - * - * @param {String} glob - Glob notation to be checked. - * @returns {Boolean} - - */ - static hasMagic(glob) { - return Glob.isValid(glob) && (re.WILDCARDS.test(glob) || glob[0] === '!'); - } - - /** - * Gets a regular expressions instance from the given glob notation. - * Note that the bang `!` prefix will be ignored if the given glob is negated. - * @name Notation.Glob.toRegExp - * @function - * - * @param {String} glob - Glob notation to be converted. - * - * @returns {RegExp} - A `RegExp` instance from the glob. - * - * @throws {NotationError} - If given notation glob is invalid. - */ - static toRegExp(glob) { - if (!Glob.isValid(glob)) { - throw new NotationError(`${ERR_INVALID} '${glob}'`); - } - - let g = glob.indexOf('!') === 0 ? glob.slice(1) : glob; - g = utils.pregQuote(g) - // `[*]` always represents array index e.g. `[1]`. so we'd replace - // `\[\*\]` with `\[\d+\]` but we should also watch for quotes: e.g. - // `["x[*]y"]` - .replace(/\\\[\\\*\\\](?=(?:[^"]|"[^"]*")*$)(?=(?:[^']|'[^']*')*$)/g, '\\[\\d+\\]') - // `*` within quotes (e.g. ['*']) is non-wildcard, just a regular star char. - // `*` outside of quotes is always JS variable syntax e.g. `prop.*` - .replace(/\\\*(?=(?:[^"]|"[^"]*")*$)(?=(?:[^']|'[^']*')*$)/g, '[a-z$_][a-z$_\\d]*') - .replace(/\\\?/g, '.'); - return new RegExp('^' + g + '(?:[\\[\\.].+|$)', 'i'); - // it should either end ($) or continue with a dot or bracket. So for - // example, `company.*` will produce `/^company\.[a-z$_][a-z$_\\d]*(?:[\\[|\\.].+|$)/` - // which will match both `company.name` and `company.address.street` but - // will not match `some.company.name`. Also `!password` will not match - // `!password_reset`. - } - - /** - * Specifies whether first glob notation can represent (or cover) the - * second. - * @name Notation.Glob._covers - * @function - * @private - * - * @param {String|Object|Glob} globA Source glob notation string - * or inspection result object or `Notation.Glob` instance. - * @param {String|Object|Glob} globB Glob notation string or - * inspection result object or `Notation.Glob` instance. - * @param {Boolean} [match=false] Check whether notes match instead of - * `globA` covers `globB`. - * @returns {Boolean} - - * - * @example - * const { covers } = Notation.Glob; - * covers('*.y', 'x.y') // true - * covers('x.y', '*.y') // false - * covers('x.y', '*.y', true) // true - * covers('x[*].y', 'x[*]') // false - */ - static _covers(globA, globB, match = false) { - const a = typeof globA === 'string' - ? new Glob(globA) - : globA; // assume (globA instanceof Notation.Glob || utils.type(globA) === 'object') - - const b = typeof globB === 'string' - ? new Glob(globB) - : globB; - - const notesA = a.notes || Glob.split(a.absGlob); - const notesB = b.notes || Glob.split(b.absGlob); - - if (!match) { - // !x.*.* does not cover !x.* or x.* bec. !x.*.* ≠ x.* ≠ x - // x.*.* covers x.* bec. x.*.* = x.* = x - if (a.isNegated && notesA.length > notesB.length) return false; - } - - let covers = true; - const fn = match ? _matchesNote : _coversNote; - for (let i = 0; i < notesA.length; i++) { - if (!fn(notesA[i], notesB[i])) { - covers = false; - break; - } - } - return covers; - } - - /** - * Gets the intersection notation of two glob notations. When restrictive, - * if any one of them is negated, the outcome is negated. Otherwise, only - * if both of them are negated, the outcome is negated. - * @name Notation.Glob._intersect - * @function - * @private - * - * @param {String} globA - First glob to be used. - * @param {String} globB - Second glob to be used. - * @param {Boolean} [restrictive=false] - Whether the intersection should - * be negated when one of the globs is negated. - * @returns {String} - Intersection notation if any; otherwise `null`. - * @example - * _intersect('!*.y', 'x.*', false) // 'x.y' - * _intersect('!*.y', 'x.*', true) // '!x.y' - */ - static _intersect(globA, globB, restrictive = false) { - // const bang = restrictive - // ? (globA[0] === '!' || globB[0] === '!' ? '!' : '') - // : (globA[0] === '!' && globB[0] === '!' ? '!' : ''); - - const notesA = Glob.split(globA, true); - const notesB = Glob.split(globB, true); - - let bang; - if (restrictive) { - bang = globA[0] === '!' || globB[0] === '!' ? '!' : ''; - } else { - if (globA[0] === '!' && globB[0] === '!') { - bang = '!'; - } else { - bang = ((notesA.length > notesB.length && globA[0] === '!') - || (notesB.length > notesA.length && globB[0] === '!')) - ? '!' - : ''; - } - } - - const len = Math.max(notesA.length, notesB.length); - let notesI = []; - let a, b; - // x.* ∩ *.y » x.y - // x.*.* ∩ *.y » x.y.* - // x.*.z ∩ *.y » x.y.z - // x.y ∩ *.b » (n/a) - // x.y ∩ a.* » (n/a) - for (let i = 0; i < len; i++) { - a = notesA[i]; - b = notesB[i]; - if (a === b) { - notesI.push(a); - } else if (a && re.WILDCARD.test(a)) { - if (!b) { - notesI.push(a); - } else { - notesI.push(b); - } - } else if (b && re.WILDCARD.test(b)) { - if (!a) { - notesI.push(b); - } else { - notesI.push(a); - } - } else if (a && !b) { - notesI.push(a); - } else if (!a && b) { - notesI.push(b); - } else { // if (a !== b) { - notesI = []; - break; - } - } - - if (notesI.length > 0) return bang + utils.joinNotes(notesI); - return null; - } - - /** - * Undocumented. - * @name Notation.Glob._inspect - * @function - * @private - * - * @param {String} glob - - * @returns {Object} - - */ - static _inspect(glob) { - let g = glob.trim(); - if (!Glob.isValid(g)) { - throw new NotationError(`${ERR_INVALID} '${glob}'`); - } - const isNegated = g[0] === '!'; - // trailing wildcards are only redundant if not negated - if (!isNegated) g = utils.removeTrailingWildcards(g); - const absGlob = isNegated ? g.slice(1) : g; - return { - glob: g, - absGlob, - isNegated, - // e.g. [*] or [1] are array globs. ["1"] is not. - isArrayGlob: (/^\[[^'"]/).test(absGlob) - }; - } - - /** - * Splits the given glob notation string into its notes (levels). Note that - * this will exclude the `!` negation prefix, if it exists. - * @name Notation.Glob.split - * @function - * - * @param {String} glob Glob notation string to be splitted. - * @param {String} [normalize=false] Whether to remove trailing, redundant - * wildcards. - * @returns {Array} - A string array of glob notes (levels). - * @throws {NotationError} - If given glob notation is invalid. - * - * @example - * Notation.Glob.split('*.list[2].prop') // ['*', 'list', '[2]', 'prop'] - * // you can get the same result from the .notes property of a Notation.Glob instance. - */ - static split(glob, normalize = false) { - if (!Glob.isValid(glob)) { - throw new NotationError(`${ERR_INVALID} '${glob}'`); - } - const neg = glob[0] === '!'; - // trailing wildcards are redundant only when not negated - const g = !neg && normalize ? utils.removeTrailingWildcards(glob) : glob; - return g.replace(/^!/, '').match(reMATCHER); - } - - /** - * Compares two given notation globs and returns an integer value as a - * result. This is generally used to sort glob arrays. Loose globs (with - * stars especially closer to beginning of the glob string) and globs - * representing the parent/root of the compared property glob come first. - * Verbose/detailed/exact globs come last. (`* < *.abc < abc`). - * - * For instance; `store.address` comes before `store.address.street`. So - * this works both for `*, store.address.street, !store.address` and `*, - * store.address, !store.address.street`. For cases such as `prop.id` vs - * `!prop.id` which represent the same property; the negated glob comes - * last. - * @name Notation.Glob.compare - * @function - * - * @param {String} globA - First notation glob to be compared. - * @param {String} globB - Second notation glob to be compared. - * - * @returns {Number} - Returns `-1` if `globA` comes first, `1` if `globB` - * comes first and `0` if equivalent priority. - * - * @throws {NotationError} - If either `globA` or `globB` is invalid glob - * notation. - * - * @example - * const { compare } = Notation.Glob; - * compare('*', 'info.user') // -1 - * compare('*', '[*]') // 0 - * compare('info.*.name', 'info.user') // 1 - */ - static compare(globA, globB) { - // trivial case, both are exactly the same! - // or both are wildcard e.g. `*` or `[*]` - if (globA === globB || (re.WILDCARD.test(globA) && re.WILDCARD.test(globB))) return 0; - - const a = new Glob(globA); - const b = new Glob(globB); - - // Check depth (number of levels) - if (a.notes.length === b.notes.length) { - // check and compare if these are globs that represent items in the - // "same" array. if not, this will return 0. - const aIdxCompare = _compareArrayItemGlobs(a, b); - // we'll only continue comparing if 0 is returned - if (aIdxCompare !== 0) return aIdxCompare; - - // count wildcards - const wildCountA = (a.absGlob.match(re.WILDCARDS) || []).length; - const wildCountB = (b.absGlob.match(re.WILDCARDS) || []).length; - if (wildCountA === wildCountB) { - // check for negation - if (!a.isNegated && b.isNegated) return -1; - if (a.isNegated && !b.isNegated) return 1; - // both are negated or neither are, return alphabetical - return a.absGlob < b.absGlob ? -1 : (a.absGlob > b.absGlob ? 1 : 0); - } - return wildCountA > wildCountB ? -1 : 1; - } - - return a.notes.length < b.notes.length ? -1 : 1; - } - - /** - * Sorts the notation globs in the given array by their priorities. Loose - * globs (with stars especially closer to beginning of the glob string); - * globs representing the parent/root of the compared property glob come - * first. Verbose/detailed/exact globs come last. (`* < *.y < x.y`). - * - * For instance; `store.address` comes before `store.address.street`. For - * cases such as `prop.id` vs `!prop.id` which represent the same property; - * the negated glob wins (comes last). - * @name Notation.Glob.sort - * @function - * - * @param {Array} globList - The notation globs array to be sorted. The - * passed array reference is modified. - * @returns {Array} - Logically sorted globs array. - * - * @example - * Notation.Glob.sort(['!prop.*.name', 'prop.*', 'prop.id']) // ['prop.*', 'prop.id', '!prop.*.name']; - */ - static sort(globList) { - return globList.sort(Glob.compare); - } - - /** - * Normalizes the given notation globs array by removing duplicate or - * redundant items, eliminating extra verbosity (also with intersection - * globs) and returns a priority-sorted globs array. - * - *
    - *
  • If any exact duplicates found, all except first is removed. - *
    example: `['car', 'dog', 'car']` normalizes to `['car', 'dog']`.
  • - *
  • If both normal and negated versions of a glob are found, negated wins. - *
    example: `['*', 'id', '!id']` normalizes to `['*', '!id']`.
  • - *
  • If a glob is covered by another, it's removed. - *
    example: `['car.*', 'car.model']` normalizes to `['car']`.
  • - *
  • If a negated glob is covered by another glob, it's kept. - *
    example: `['*', 'car', '!car.model']` normalizes as is.
  • - *
  • If a negated glob is not covered by another or it does not cover any other; - * then we check for for intersection glob. If found, adds them to list; - * removes the original negated. - *
    example: `['car.*', '!*.model']` normalizes as to `['car', '!car.model']`.
  • - *
  • In restrictive mode; if a glob is covered by another negated glob, it's removed. - * Otherwise, it's kept. - *
    example: `['*', '!car.*', 'car.model']` normalizes to `['*', '!car']` if restrictive.
  • - *
- * @name Notation.Glob.normalize - * @function - * - * @param {Array} globList - Notation globs array to be normalized. - * @param {Boolean} [restrictive=false] - Whether negated items strictly - * remove every match. Note that, regardless of this option, if any item has an - * exact negated version; non-negated is always removed. - * @returns {Array} - - * - * @throws {NotationError} - If any item in globs list is invalid. - * - * @example - * const { normalize } = Notation.Glob; - * normalize(['*', '!id', 'name', '!car.model', 'car.*', 'id', 'name']); // ['*', '!id', '!car.model'] - * normalize(['!*.id', 'user.*', 'company']); // ['company', 'user', '!company.id', '!user.id'] - * normalize(['*', 'car.model', '!car.*']); // ["*", "!car.*", "car.model"] - * // restrictive normalize: - * normalize(['*', 'car.model', '!car.*'], true); // ["*", "!car.*"] - */ - static normalize(globList, restrictive = false) { - const { _inspect, _covers, _intersect } = Glob; - - const original = utils.ensureArray(globList); - if (original.length === 0) return []; - - const list = original - // prevent mutation - .concat() - // move negated globs to top so that we inspect non-negated globs - // against others first. when complete, we'll sort with our - // .compare() function. - .sort(restrictive ? _negFirstSort : _negLastSort) - // turning string array into inspect-obj array, so that we'll not - // run _inspect multiple times in the inner loop. this also - // pre-validates each glob. - .map(_inspect); - - // early return if we have a single item - if (list.length === 1) { - const g = list[0]; - // single negated item is redundant - if (g.isNegated) return []; - // return normalized - return [g.glob]; - } - - // flag to return an empty array (in restrictive mode), if true. - let negateAll = false; - - // we'll push keepers in this array - let normalized = []; - // we'll need to remember excluded globs, so that we can move to next - // item early. - const ignored = {}; - - // storage to keep intersections. - // using an object to prevent duplicates. - let intersections = {}; - - function checkAddIntersection(gA, gB) { - const inter = _intersect(gA, gB, restrictive); - if (!inter) return; - // if the intersection result has an inverted version in the - // original list, don't add this. - const hasInverted = restrictive ? false : original.indexOf(_invert(inter)) >= 0; - // also if intersection result is in the current list, don't add it. - if (list.indexOf(inter) >= 0 || hasInverted) return; - intersections[inter] = inter; - } - - // iterate each glob by comparing it to remaining globs. - utils.eachRight(list, (a, indexA) => { - - // if `strict` is enabled, return empty if a negate-all is found - // (which itself is also redundant if single): '!*' or '![*]' - if (re.NEGATE_ALL.test(a.glob)) { - negateAll = true; - if (restrictive) return false; - } - - // flags - let duplicate = false; - let hasExactNeg = false; - // flags for negated - let negCoversPos = false; - let negCoveredByPos = false; - let negCoveredByNeg = false; - // flags for non-negated (positive) - let posCoversPos = false; - let posCoveredByNeg = false; - let posCoveredByPos = false; - - utils.eachRight(list, (b, indexB) => { - // don't inspect glob with itself - if (indexA === indexB) return; // move to next - // console.log(indexA, a.glob, 'vs', b.glob); - - // e.g. ['x.y.z', '[1].x', 'c'] » impossible! the tested source - // object cannot be both an array and an object. - if (a.isArrayGlob !== b.isArrayGlob) { - throw new NotationError(`Integrity failed. Cannot have both object and array notations for root level: ${JSON.stringify(original)}`); - } - - // remove if duplicate - if (a.glob === b.glob) { - list.splice(indexA, 1); - duplicate = true; - return false; // break out - } - - // remove if positive has an exact negated (negated wins when - // normalized) e.g. ['*', 'a', '!a'] => ['*', '!a'] - if (!a.isNegated && _isReverseOf(a, b)) { - // list.splice(indexA, 1); - ignored[a.glob] = true; - hasExactNeg = true; - return false; // break out - } - - // if already excluded b, go on to next - if (ignored[b.glob]) return; // next - - const coversB = _covers(a, b); - const coveredByB = coversB ? false : _covers(b, a); - if (a.isNegated) { - if (b.isNegated) { - // if negated (a) covered by any other negated (b); remove (a)! - if (coveredByB) { - negCoveredByNeg = true; - // list.splice(indexA, 1); - ignored[a.glob] = true; - return false; // break out - } - } else { - /* istanbul ignore if */ - if (coversB) negCoversPos = true; - if (coveredByB) negCoveredByPos = true; - // try intersection if none covers the other and only - // one of them is negated. - if (!coversB && !coveredByB) { - checkAddIntersection(a.glob, b.glob); - } - } - } else { - if (b.isNegated) { - // if positive (a) covered by any negated (b); remove (a)! - if (coveredByB) { - posCoveredByNeg = true; - if (restrictive) { - // list.splice(indexA, 1); - ignored[a.glob] = true; - return false; // break out - } - return; // next - } - // try intersection if none covers the other and only - // one of them is negated. - if (!coversB && !coveredByB) { - checkAddIntersection(a.glob, b.glob); - } - } else { - if (coversB) posCoversPos = coversB; - // if positive (a) covered by any other positive (b); remove (a)! - if (coveredByB) { - posCoveredByPos = true; - if (restrictive) { - // list.splice(indexA, 1); - return false; // break out - } - } - } - } - - }); - - // const keepNeg = (negCoversPos || negCoveredByPos) && !negCoveredByNeg; - const keepNeg = restrictive - ? (negCoversPos || negCoveredByPos) && negCoveredByNeg === false - : negCoveredByPos && negCoveredByNeg === false; - const keepPos = restrictive - ? (posCoversPos || posCoveredByPos === false) && posCoveredByNeg === false - : posCoveredByNeg || posCoveredByPos === false; - const keep = duplicate === false - && hasExactNeg === false - && (a.isNegated ? keepNeg : keepPos); - - if (keep) { - normalized.push(a.glob); - } else { - // this is excluded from final (normalized) list, so mark as - // ignored (don't remove from "list" for now) - ignored[a.glob] = true; - } - }); - - if (restrictive && negateAll) return []; - - intersections = Object.keys(intersections); - if (intersections.length > 0) { - // merge normalized list with intersections if any - normalized = normalized.concat(intersections); - // we have new (intersection) items, so re-normalize - return Glob.normalize(normalized, restrictive); - } - - return Glob.sort(normalized); - } - - /** - * Undocumented. See `.union()` - * @name Notation.Glob._compareUnion - * @function - * @private - * - * @param {Array} globsListA - - * @param {Array} globsListB - - * @param {Boolean} restrictive - - * @param {Array} union - - * @returns {Array} - - */ - static _compareUnion(globsListA, globsListB, restrictive, union = []) { - const { _covers } = Glob; - - const { _inspect, _intersect } = Glob; - - utils.eachRight(globsListA, globA => { - if (union.indexOf(globA) >= 0) return; // next - - const a = _inspect(globA); - - // if wildcard only, add... - if (re.WILDCARD.test(a.absGlob)) { - union.push(a.glob); // push normalized glob - return; // next - } - - let notCovered = false; - let hasExact = false; - let negCoversNeg = false; - let posCoversNeg = false; - let posCoversPos = false; - let negCoversPos = false; - - const intersections = []; - - utils.eachRight(globsListB, globB => { - - // keep if has exact in the other - if (globA === globB) hasExact = true; - - const b = _inspect(globB); - - // keep negated if: - // 1) any negated covers it - // 2) no positive covers it - // keep positive if: - // 1) no positive covers it OR any negated covers it - - notCovered = !_covers(b, a); - if (notCovered) { - if (a.isNegated && b.isNegated) { - const inter = _intersect(a.glob, b.glob, restrictive); - if (inter && union.indexOf(inter) === -1) intersections.push(inter); - } - return; // next - } - - if (a.isNegated) { - if (b.isNegated) { - negCoversNeg = !hasExact; - } else { - posCoversNeg = true; // set flag - } - } else { - if (!b.isNegated) { - posCoversPos = !hasExact; - } else { - negCoversPos = true; // set flag - } - } - - }); - - - const keep = a.isNegated - ? (!posCoversNeg || negCoversNeg) - : (!posCoversPos || negCoversPos); - - if (hasExact || keep || (notCovered && !a.isNegated)) { - union.push(a.glob); // push normalized glob - return; - } - - if (a.isNegated && posCoversNeg && !negCoversNeg && intersections.length > 0) { - union = union.concat(intersections); - } - - }); - - return union; - } - - /** - * Gets the union from the given couple of glob arrays and returns a new - * array of globs. - *
    - *
  • If the exact same element is found in both - * arrays, one of them is removed to prevent duplicates. - *
    example: `['!id', 'name'] ∪ ['!id']` unites to `['!id', 'name']`
  • - *
  • If any non-negated item is covered by a glob in the same - * or other array, the redundant item is removed. - *
    example: `['*', 'name'] ∪ ['email']` unites to `['*']`
  • - *
  • If one of the arrays contains a negated equivalent of an - * item in the other array, the negated item is removed. - *
    example: `['!id'] ∪ ['id']` unites to `['id']`
  • - *
  • If any item covers/matches a negated item in the other array, - * the negated item is removed. - *
    example #1: `['!user.id'] ∪ ['user.*']` unites to `['user']` - *
    example #2: `['*'] ∪ ['!password']` unites to `['*']` - *
  • - *
- * @name Notation.Glob.union - * @function - * - * @param {Array} globsA - First array of glob strings. - * @param {Array} globsB - Second array of glob strings. - * @param {Boolean} [restrictive=false] - Whether negated items in each of - * the lists, strictly remove every match in themselves (not the cross - * list). This option is used when pre-normalizing each glob list and - * normalizing the final union list. - * - * @returns {Array} - - * - * @example - * const a = ['user.*', '!user.email', 'car.model', '!*.id']; - * const b = ['!*.date', 'user.email', 'car', '*.age']; - * const { union } = Notation.Glob; - * union(a, b) // ['car', 'user', '*.age', '!car.date', '!user.id'] - */ - static union(globsA, globsB, restrictive) { - const { normalize, _compareUnion } = Glob; - - const listA = normalize(globsA, restrictive); - const listB = normalize(globsB, restrictive); - - if (listA.length === 0) return listB; - if (listB.length === 0) return listA; - - // TODO: below should be optimized - let union = _compareUnion(listA, listB, restrictive); - union = _compareUnion(listB, listA, restrictive, union); - return normalize(union, restrictive); - } - -} - -// -------------------------------- -// HELPERS -// -------------------------------- - -// used by static _covers -function _coversNote(a, b) { - if (!a || !b) return false; // glob e.g.: [2] does not cover [2][1] - const bIsArr = re.ARRAY_GLOB_NOTE.test(b); - // obj-wildcard a will cover b if not array - if (a === '*') return !bIsArr; - // arr-wildcard a will cover b if array - if (a === '[*]') return bIsArr; - // seems, a is not wildcard so, - // if b is wildcard (obj or arr) won't be covered - if (re.WILDCARD.test(b)) return false; - // normalize both and check for equality - // e.g. x.y and x['y'] are the same - return utils.normalizeNote(a) === utils.normalizeNote(b); -} -// function _coversNote(a, b) { -// if (!a || !b) return false; // glob e.g.: [2] does not cover [2][1] -// a = utils.normalizeNote(a, true); -// b = utils.normalizeNote(b, true); -// if (a === b) return true; -// const bIsArr = re.ARRAY_GLOB_NOTE.test(b); -// return (a === '*' && !bIsArr) || (a === '[*]' && bIsArr); -// } -// used by static _covers -function _matchesNote(a, b) { - if (!a || !b) return true; // glob e.g.: [2][1] matches [2] and vice-versa. - return _coversNote(a, b) || _coversNote(b, a); -} - -// used by _compareArrayItemGlobs() for getting a numeric index from array note. -// we'll use these indexes to sort higher to lower, as removing order; to -// prevent shifted indexes. -function _idxVal(note) { - // we return -1 for wildcard bec. we need it to come last - - // below will never execute when called from _compareArrayItemGlobs - /* istanbul ignore next */ - // if (note === '[*]') return -1; - - // e.g. '[2]' » 2 - return parseInt(note.replace(/[[\]]/, ''), 10); -} - -function _compArrIdx(lastA, lastB) { - const iA = _idxVal(lastA); - const iB = _idxVal(lastB); - - // below will never execute when called from _compareArrayItemGlobs - /* istanbul ignore next */ - // if (iA === iB) return 0; - - return iA > iB ? -1 : 1; -} - -// when we remove items from an array (via e.g. filtering), we first need to -// remove the item with the greater index so indexes of other items (that are to -// be removed from the same array) do not shift. so below is for comparing 2 -// globs if they represent 2 items from the same array. - -// example items from same array: ![*][2] ![0][*] ![0][1] ![0][3] -// should be sorted as ![0][3] ![*][2] ![0][1] ![0][*] -function _compareArrayItemGlobs(a, b) { - const reANote = re.ARRAY_GLOB_NOTE; - // both should be negated - if (!a.isNegated - || !b.isNegated - // should be same length (since we're comparing for items in same - // array) - || a.notes.length !== b.notes.length - // last notes should be array brackets - || !reANote.test(a.last) - || !reANote.test(b.last) - // last notes should be different to compare - || a.last === b.last - ) return 0; - - // negated !..[*] should come last - if (a.last === '[*]') return 1; // b is first - if (b.last === '[*]') return -1; // a is first - - if (a.parent && b.parent) { - const { _covers } = Glob; - if (_covers(a.parent, b.parent, true)) { - return _compArrIdx(a.last, b.last); - } - return 0; - } - return _compArrIdx(a.last, b.last); -} - -// x vs !x.*.* » false -// x vs !x[*] » true -// x[*] vs !x » true -// x[*] vs !x[*] » false -// x.* vs !x.* » false -function _isReverseOf(a, b) { - return a.isNegated !== b.isNegated - && a.absGlob === b.absGlob; -} - -function _invert(glob) { - return glob[0] === '!' ? glob.slice(1) : '!' + glob; -} - -const _rx = /^\s*!/; -function _negFirstSort(a, b) { - const negA = _rx.test(a); - const negB = _rx.test(b); - if (negA && negB) return a.length >= b.length ? 1 : -1; - if (negA) return -1; - if (negB) return 1; - return 0; -} -function _negLastSort(a, b) { - const negA = _rx.test(a); - const negB = _rx.test(b); - if (negA && negB) return a.length >= b.length ? 1 : -1; - if (negA) return 1; - if (negB) return -1; - return 0; -} - -// -------------------------------- -// EXPORT -// -------------------------------- - -export { Glob }; diff --git a/src/core/notation.js b/src/core/notation.js deleted file mode 100644 index dfe8763..0000000 --- a/src/core/notation.js +++ /dev/null @@ -1,1443 +0,0 @@ -/* eslint no-use-before-define:0, consistent-return:0, max-statements:0, max-len:0 */ - -import { Glob } from './notation.glob'; -import { NotationError } from './notation.error'; -import { utils } from '../utils'; - -const ERR = { - SOURCE: 'Invalid source. Expected a data object or array.', - DEST: 'Invalid destination. Expected a data object or array.', - NOTATION: 'Invalid notation: ', - NOTA_OBJ: 'Invalid notations object. ', - NO_INDEX: 'Implied index does not exist: ', - NO_PROP: 'Implied property does not exist: ' -}; - -// created test @ https://regex101.com/r/vLE16M/2 -const reMATCHER = /(\[(\d+|".*"|'.*'|`.*`)\]|[a-z$_][a-z$_\d]*)/gi; -// created test @ https://regex101.com/r/fL3PJt/1/ -// /^([a-z$_][a-z$_\d]*|\[(\d+|".*"|'.*'|`.*`)\])(\[(\d+|".*"|'.*'|`.*`)\]|(\.[a-z$_][a-z$_\d]*))*$/i -const reVALIDATOR = new RegExp( - '^(' - + '[a-z$_][a-z$_\\d]*' // JS variable syntax - + '|' // OR - + '\\[(\\d+|".*"|\'.*\')\\]' // array index or object bracket notation - + ')' // exactly once - + '(' - + '\\[(\\d+|".*"|\'.*\')\\]' // followed by same - + '|' // OR - + '\\.[a-z$_][a-z$_\\d]*' // dot, then JS variable syntax - + ')*' // (both) may repeat any number of times - + '$' - , 'i' -); - -const DEFAULT_OPTS = Object.freeze({ - strict: false, - preserveIndices: false -}); - -/** - * Notation.js for Node and Browser. - * - * Like in most programming languages, JavaScript makes use of dot-notation to - * access the value of a member of an object (or class). `Notation` class - * provides various methods for modifying / processing the contents of the - * given object; by parsing object notation strings or globs. - * - * Note that this class will only deal with enumerable properties of the source - * object; so it should be used to manipulate data objects. It will not deal - * with preserving the prototype-chain of the given object. - * - * @author Onur Yıldırım - * @license MIT - */ -class Notation { - - /** - * Initializes a new instance of `Notation`. - * - * @param {Object|Array} [source={}] - The source object (or array) to be - * notated. Can either be an array or object. If omitted, defaults to an - * empty object. - * @param {Object} [options] - Notation options. - * @param {Boolean} [options.strict=false] - Whether to throw either when - * a notation path does not exist on the source (i.e. `#get()` and `#remove()` - * methods); or notation path exists but overwriting is disabled (i.e. - * `#set()` method). (Note that `.inspectGet()` and `.inspectRemove()` methods - * are exceptions). It's recommended to set this to `true` and prevent silent - * failures if you're working with sensitive data. Regardless of `strict` option, - * it will always throw on invalid notation syntax or other crucial failures. - * - * @example - * const obj = { car: { brand: "Dodge", model: "Charger", year: 1970 } }; - * const notation = new Notation(obj); - * notation.get('car.model') // » "Charger" - * notation.remove('car.model').set('car.color', 'red').value - * // » { car: { brand: "Dodge", year: 1970, color: "red" } } - */ - constructor(source, options) { - if (arguments.length === 0) { - this._source = {}; - } else if (!utils.isCollection(source)) { - throw new NotationError(ERR.SOURCE); - } else { - this._source = source; - } - - this._isArray = utils.type(this._source) === 'array'; - this.options = options; - } - - // -------------------------------- - // INSTANCE PROPERTIES - // -------------------------------- - - /** - * Gets or sets notation options. - * @type {Object} - */ - get options() { - return this._options; - } - - set options(value) { - this._options = { - ...DEFAULT_OPTS, - ...(this._options || {}), - ...(value || {}) - }; - } - - /** - * Gets the value of the source object. - * @type {Object|Array} - * - * @example - * const person = { name: "Onur" }; - * const me = Notation.create(person) - * .set("age", 36) - * .set("car.brand", "Ford") - * .set("car.model", "Mustang") - * .value; - * console.log(me); // { name: "Onur", age: 36, car: { brand: "Ford", model: "Mustang" } } - * console.log(person === me); // true - */ - get value() { - return this._source; - } - - // -------------------------------- - // INSTANCE METHODS - // -------------------------------- - - /** - * Recursively iterates through each key of the source object and invokes - * the given callback function with parameters, on each non-object value. - * - * @param {Function} callback - The callback function to be invoked on - * each on each non-object value. To break out of the loop, return `false` - * from within the callback. - * Callback signature: `callback(notation, key, value, object) { ... }` - * - * @returns {Notation} - The current `Notation` instance (self). - * - * @example - * const obj = { car: { brand: "Dodge", model: "Charger", year: 1970 } }; - * Notation.create(obj).each(function (notation, key, value, object) { - * console.log(notation, value); - * }); - * // "car.brand" "Dodge" - * // "car.model" "Charger" - * // "car.year" 1970 - */ - each(callback) { - _each(this._source, callback); - return this; - } - - /** - * Iterates through each note of the given notation string by evaluating - * it on the source object. - * - * @param {String} notation - The notation string to be iterated through. - * @param {Function} callback - The callback function to be invoked on - * each iteration. To break out of the loop, return `false` from within - * the callback. Signature: `callback(levelValue, note, index, list)` - * - * @returns {Notation} - The current `Notation` instance (self). - * - * @example - * const obj = { car: { brand: "Dodge", model: "Charger", year: 1970 } }; - * Notation.create(obj) - * .eachValue("car.brand", function (levelValue, note, index, list) { - * console.log(note, levelValue); // "car.brand" "Dodge" - * }); - */ - eachValue(notation, callback) { - let level = this._source; - Notation.eachNote(notation, (levelNotation, note, index, list) => { - level = utils.hasOwn(level, note) ? level[note] : undefined; - if (callback(level, levelNotation, note, index, list) === false) return false; - - }); - return this; - } - - /** - * Gets the list of notations from the source object (keys). - * - * @returns {Array} - An array of notation strings. - * - * @example - * const obj = { car: { brand: "Dodge", model: "Charger", year: 1970 } }; - * const notations = Notation.create(obj).getNotations(); - * console.log(notations); // [ "car.brand", "car.model", "car.year" ] - */ - getNotations() { - const list = []; - this.each(notation => { - list.push(notation); - }); - return list; - } - - /** - * Deeply clones the source object. This is also useful if you want to - * prevent mutating the original source object. - * - *
- * Note that `Notation` expects a data object (or array) with enumerable - * properties. In addition to plain objects and arrays; supported cloneable - * property/value types are primitives (such as `String`, `Number`, - * `Boolean`, `Symbol`, `null` and `undefined`) and built-in types (such as - * `Date` and `RegExp`). - * - * Enumerable properties with types other than these (such as methods, - * special objects, custom class instances, etc) will be copied by reference. - * Non-enumerable properties will not be cloned. - * - * If you still need full clone support, you can use a library like lodash. - * e.g. `Notation.create(_.cloneDeep(source))` - *
- * - * @returns {Notation} - The current `Notation` instance (self). - * - * @example - * const mutated = Notation.create(source1).set('newProp', true).value; - * console.log(source1.newProp); // ——» true - * - * const cloned = Notation.create(source2).clone().set('newProp', true).value; - * console.log('newProp' in source2); // ——» false - * console.log(cloned.newProp); // ——» true - */ - clone() { - this._source = utils.cloneDeep(this._source); - return this; - } - - /** - * Flattens the source object to a single-level object with notated keys. - * - * @returns {Notation} - The current `Notation` instance (self). - * - * @example - * const obj = { car: { brand: "Dodge", model: "Charger", year: 1970 } }; - * console.log(Notation.create(obj).flatten().value); - * // { - * // "car.brand": "Dodge", - * // "car.model": "Charger", - * // "car.year": 1970 - * // } - */ - flatten() { - const o = {}; - this.each((notation, key, value) => { - o[notation] = value; - }); - this._source = o; - return this; - } - - /** - * Aggregates notated keys of a (single-level) object, and nests them under - * their corresponding properties. This is the opposite of `Notation#flatten` - * method. This might be useful when expanding a flat object fetched from - * a database. - * @alias Notation#aggregate - * @chainable - * - * @returns {Notation} - The current `Notation` instance (self). - * - * @example - * const obj = { "car.brand": "Dodge", "car.model": "Charger", "car.year": 1970 } - * const expanded = Notation.create(obj).expand().value; - * console.log(expanded); // { car: { brand: "Dodge", model: "Charger", year: 1970 } }; - */ - expand() { - this._source = Notation.create({}).merge(this._source).value; - return this; - } - - /** - * Alias for `#expand` - * @private - * @returns {Notation} - - */ - aggregate() { - return this.expand(); - } - - /** - * Inspects the given notation on the source object by checking - * if the source object actually has the notated property; - * and getting its value if exists. - * @param {String} notation - The notation string to be inspected. - * @returns {InspectResult} - The result object. - * - * @example - * Notation.create({ car: { year: 1970 } }).inspectGet("car.year"); - * // { has: true, value: 1970, lastNote: 'year', lastNoteNormalized: 'year' } - * Notation.create({ car: { year: 1970 } }).inspectGet("car.color"); - * // { has: false } - * Notation.create({ car: { color: undefined } }).inspectGet("car.color"); - * // { has: true, value: undefined, lastNote: 'color', lastNoteNormalized: 'color' } - * Notation.create({ car: { brands: ['Ford', 'Dodge'] } }).inspectGet("car.brands[1]"); - * // { has: true, value: 'Dodge', lastNote: '[1]', lastNoteNormalized: 1 } - */ - inspectGet(notation) { - let level = this._source; - let result = { has: false, value: undefined }; - let parent; - Notation.eachNote(notation, (levelNotation, note, index) => { - const lastNoteNormalized = utils.normalizeNote(note); - if (utils.hasOwn(level, lastNoteNormalized)) { - level = level[lastNoteNormalized]; - parent = level; - result = { - notation, - has: true, - value: level, - type: utils.type(level), - level: index + 1, - lastNote: note, - lastNoteNormalized - }; - } else { - // level = undefined; - result = { - notation, - has: false, - type: 'undefined', - level: index + 1, - lastNote: note, - lastNoteNormalized - }; - return false; // break out - } - }); - - if (parent === undefined || (result.has && parent === result.value)) parent = this._source; - result.parentIsArray = utils.type(parent) === 'array'; - - return result; - } - - /** - * Notation inspection result object. - * @typedef Notation~InspectResult - * @type Object - * @property {String} notation - Notation that is inspected. - * @property {Boolean} has - Indicates whether the source object has the - * given notation as a (leveled) enumerable property. If the property - * exists but has a value of `undefined`, this will still return `true`. - * @property {*} value - The value of the notated property. If the source - * object does not have the notation, the value will be `undefined`. - * @property {String} type - The type of the notated property. If the source - * object does not have the notation, the type will be `"undefined"`. - * @property {String} lastNote - Last note of the notation, if actually - * exists. For example, last note of `'a.b.c'` is `'c'`. - * @property {String|Number} lastNoteNormalized - Normalized representation - * of the last note of the notation, if actually exists. For example, last - * note of `'a.b[1]` is `'[1]'` and will be normalized to number `1`; which - * indicates an array index. - * @property {Boolean} parentIsArray - Whether the parent object of the - * notation path is an array. - */ - - /** - * Inspects and removes the given notation from the source object by - * checking if the source object actually has the notated property; and - * getting its value if exists, before removing the property. - * - * @param {String} notation - The notation string to be inspected. - * - * @returns {InspectResult} - The result object. - * - * @example - * const obj = { name: "John", car: { year: 1970 } }; - * let result = Notation.create(obj).inspectRemove("car.year"); - * // result » { notation: "car.year", has: true, value: 1970, lastNote: "year", lastNoteNormalized: "year" } - * // obj » { name: "John", car: {} } - * - * result = Notation.create({ car: { year: 1970 } }).inspectRemove("car.color"); - * // result » { notation: "car.color", has: false } - * Notation.create({ car: { color: undefined } }).inspectRemove("car['color']"); - * // { notation: "car.color", has: true, value: undefined, lastNote: "['color']", lastNoteNormalized: "color" } - * - * const obj = { car: { colors: ["black", "white"] } }; - * const result = Notation.create().inspectRemove("car.colors[0]"); - * // result » { notation: "car.colors[0]", has: true, value: "black", lastNote: "[0]", lastNoteNormalized: 0 } - * // obj » { car: { colors: [(empty), "white"] } } - */ - inspectRemove(notation) { - if (!notation) throw new Error(ERR.NOTATION + `'${notation}'`); - const parentNotation = Notation.parent(notation); - const parent = parentNotation ? this.get(parentNotation, null) : this._source; - const parentIsArray = utils.type(parent) === 'array'; - const notes = Notation.split(notation); - const lastNote = notes[notes.length - 1]; - const lastNoteNormalized = utils.normalizeNote(lastNote); - - let result, value; - if (utils.hasOwn(parent, lastNoteNormalized)) { - value = parent[lastNoteNormalized]; - result = { - notation, - has: true, - value, - type: utils.type(value), - level: notes.length, - lastNote, - lastNoteNormalized, - parentIsArray - }; - - // if `preserveIndices` is enabled and this is an array, we'll - // splice the item out. otherwise, we'll use `delete` syntax to - // empty the item. - if (!this.options.preserveIndices && parentIsArray) { - parent.splice(lastNoteNormalized, 1); - } else { - delete parent[lastNoteNormalized]; - } - } else { - result = { - notation, - has: false, - type: 'undefined', - level: notes.length, - lastNote, - lastNoteNormalized, - parentIsArray - }; - } - - return result; - } - - /** - * Checks whether the source object has the given notation - * as a (leveled) enumerable property. If the property exists - * but has a value of `undefined`, this will still return `true`. - * @param {String} notation - The notation string to be checked. - * @returns {Boolean} - - * - * @example - * Notation.create({ car: { year: 1970 } }).has("car.year"); // true - * Notation.create({ car: { year: undefined } }).has("car.year"); // true - * Notation.create({}).has("car.color"); // false - */ - has(notation) { - return this.inspectGet(notation).has; - } - - /** - * Checks whether the source object has the given notation - * as a (leveled) defined enumerable property. If the property - * exists but has a value of `undefined`, this will return `false`. - * @param {String} notation - The notation string to be checked. - * @returns {Boolean} - - * - * @example - * Notation.create({ car: { year: 1970 } }).hasDefined("car.year"); // true - * Notation.create({ car: { year: undefined } }).hasDefined("car.year"); // false - * Notation.create({}).hasDefined("car.color"); // false - */ - hasDefined(notation) { - return this.inspectGet(notation).value !== undefined; - } - - /** - * Gets the value of the corresponding property at the given notation. - * - * @param {String} notation - The notation string to be processed. - * @param {String} [defaultValue] - The default value to be returned if the - * property is not found or enumerable. - * - * @returns {*} - The value of the notated property. - * @throws {NotationError} - If `strict` option is enabled, `defaultValue` - * is not set and notation does not exist. - * - * @example - * Notation.create({ car: { brand: "Dodge" } }).get("car.brand"); // "Dodge" - * Notation.create({ car: {} }).get("car.model", "Challenger"); // "Challenger" - * Notation.create({ car: { model: undefined } }).get("car.model", "Challenger"); // undefined - * - * @example get value when strict option is enabled - * // strict option defaults to false - * Notation.create({ car: {} }).get("car.model"); // undefined - * Notation.create({ car: {} }, { strict: false }).get("car.model"); // undefined - * // below will throw bec. strict = true, car.model does not exist - * // and no default value is given. - * Notation.create({ car: {} }, { strict: true }).get("car.model"); - */ - get(notation, defaultValue) { - const result = this.inspectGet(notation); - // if strict and no default value is set, check if implied index or prop - // exists - if (this.options.strict && arguments.length < 2 && !result.has) { - const msg = result.parentIsArray ? ERR.NO_INDEX : ERR.NO_PROP; - throw new NotationError(msg + `'${notation}'`); - } - return result.has ? result.value : defaultValue; - } - - /** - * Sets the value of the corresponding property at the given notation. If - * the property does not exist, it will be created and nested at the - * calculated level. If it exists; its value will be overwritten by - * default. - * @chainable - * - * @param {String} notation - The notation string to be processed. - * @param {*} value - The value to be set for the notated property. - * @param {String|Boolean} [mode="overwrite"] - Write mode. By default, - * this is set to `"overwrite"` which sets the value by overwriting the - * target object property or array item at index. To insert an array item - * (by shifting the index, instead of overwriting); set to `"insert"`. To - * prevent overwriting the value if exists, explicitly set to `false`. - * - * @returns {Notation} - The current `Notation` instance (self). - * - * @throws {NotationError} - If strict notation is enabled, `overwrite` - * option is set to `false` and attempted to overwrite an existing value. - * - * @example - * const obj = { car: { brand: "Dodge", year: 1970 } }; - * Notation.create(obj) - * .set("car.brand", "Ford") - * .set("car.model", "Mustang") - * .set("car.year", 1965, false) - * .set("car.color", "red") - * .set("boat", "none"); - * console.log(obj); - * // { notebook: "Mac", car: { brand: "Ford", model: "Mustang", year: 1970, color: "red" }, boat: "none" }; - */ - set(notation, value, mode = 'overwrite') { - if (!notation.trim()) throw new NotationError(ERR.NOTATION + `'${notation}'`); - if (mode === true) mode = 'overwrite'; - let level = this._source; - let currentIsLast, nCurrentNote, nNextNote, nextIsArrayNote, type; - const insertErrMsg = 'Cannot set value by inserting at index, on an object'; - - Notation.eachNote(notation, (levelNotation, note, index, list) => { - currentIsLast = index === list.length - 1; - nCurrentNote = nNextNote || utils.normalizeNote(note); - nNextNote = currentIsLast ? null : utils.normalizeNote(list[index + 1]); - type = utils.type(level); - - if (type === 'array' && typeof nCurrentNote !== 'number') { - const parent = Notation.parent(levelNotation) || 'source'; - throw new NotationError(`Cannot set string key '${note}' on array ${parent}`); - } - - // check if the property is at this level - if (utils.hasOwn(level, nCurrentNote, type)) { - // check if we're at the last level - if (currentIsLast) { - // if mode is "overwrite", assign the value. - if (mode === 'overwrite') { - level[nCurrentNote] = value; - } else if (mode === 'insert') { - if (type === 'array') { - level.splice(nCurrentNote, 0, value); - } else { - throw new NotationError(insertErrMsg); - } - } - // otherwise, will not overwrite - } else { - // if not last level; just re-reference the current level. - level = level[nCurrentNote]; - } - } else { - if (currentIsLast && type !== 'array' && mode === 'insert') { - throw new NotationError(insertErrMsg); - } - - // if next normalized note is a number, it indicates that the - // current note is actually an array. - nextIsArrayNote = typeof nNextNote === 'number'; - - // we don't have this property at this level so; if this is the - // last level, we set the value if not, we set an empty - // collection for the next level - level[nCurrentNote] = (currentIsLast ? value : (nextIsArrayNote ? [] : {})); - level = level[nCurrentNote]; - } - }); - return this; - } - - /** - * Just like the `.set()` method but instead of a single notation - * string, an object of notations and values can be passed. - * Sets the value of each corresponding property at the given - * notation. If a property does not exist, it will be created - * and nested at the calculated level. If it exists; its value - * will be overwritten by default. - * @chainable - * - * @param {Object} notationsObject - The notations object to be processed. - * This can either be a regular object with non-dotted keys - * (which will be merged to the first/root level of the source object); - * or a flattened object with notated (dotted) keys. - * @param {Boolean} [overwrite=true] - Whether to overwrite a property if - * exists. - * - * @returns {Notation} - The current `Notation` instance (self). - * - * @example - * const obj = { car: { brand: "Dodge", year: 1970 } }; - * Notation.create(obj).merge({ - * "car.brand": "Ford", - * "car.model": "Mustang", - * "car.year": 1965, - * "car.color": "red", - * "boat": "none" - * }); - * console.log(obj); - * // { car: { brand: "Ford", model: "Mustang", year: 1970, color: "red" }, boat: "none" }; - */ - merge(notationsObject, overwrite = true) { - if (utils.type(notationsObject) !== 'object') { - throw new NotationError(ERR.NOTA_OBJ + 'Expected an object.'); - } - let value; - utils.each(Object.keys(notationsObject), notation => { - value = notationsObject[notation]; - this.set(notation, value, overwrite); - }); - return this; - } - - /** - * Removes the properties by the given list of notations from the source - * object and returns a new object with the removed properties. - * Opposite of `merge()` method. - * - * @param {Array} notations - The notations array to be processed. - * - * @returns {Object} - An object with the removed properties. - * - * @example - * const obj = { car: { brand: "Dodge", year: 1970 }, notebook: "Mac" }; - * const separated = Notation.create(obj).separate(["car.brand", "boat" ]); - * console.log(separated); - * // { notebook: "Mac", car: { brand: "Ford" } }; - * console.log(obj); - * // { car: { year: 1970 } }; - */ - separate(notations) { - if (utils.type(notations) !== 'array') { - throw new NotationError(ERR.NOTA_OBJ + 'Expected an array.'); - } - const o = new Notation({}); - utils.each(notations, notation => { - const result = this.inspectRemove(notation); - o.set(notation, result.value); - }); - this._source = o._source; - return this; - } - - /** - * Deep clones the source object while filtering its properties by the - * given glob notations. Includes all matched properties and removes - * the rest. - * - * The difference between regular notations and glob-notations is that; - * with the latter, you can use wildcard stars (*) and negate the notation - * by prepending a bang (!). A negated notation will be excluded. - * - * Order of the globs does not matter; they will be logically sorted. Loose - * globs will be processed first and verbose globs or normal notations will - * be processed last. e.g. `[ "car.model", "*", "!car.*" ]` will be - * normalized and sorted as `[ "*", "!car" ]`. - * - * Passing no parameters or passing a glob of `"!*"` or `["!*"]` will empty - * the source object. See `Notation.Glob` class for more information. - * @chainable - * - * @param {Array|String} globList - Glob notation list to be processed. - * @param {Object} [options] - Filter options. - * @param {Boolean} [options.restrictive=false] - Whether negated items - * strictly remove every match. Note that, regardless of this option, if - * any item has an exact negated version; non-negated is always removed. - * - * @returns {Notation} - The current `Notation` instance (self). To get the - * filtered value, call `.value` property on the instance. - * - * @example - * const car = { brand: "Ford", model: { name: "Mustang", year: 1970 } }; - * const n = Notation.create(car); - * - * console.log(n.filter([ "*", "!model.year" ]).value); // { brand: "Ford", model: { name: "Mustang" } } - * console.log(n.filter("model.name").value); // { model: { name: "Mustang" } } - * console.log(car); // { brand: "Ford", model: { name: "Mustang", year: 1970 } } - * console.log(n.filter().value); // {} // —» equivalent to n.filter("") or n.filter("!*") - */ - filter(globList, options = {}) { - const { re } = utils; - - // ensure array, normalize and sort the globs in logical order. this - // also concats the array first (to prevent mutating the original - // array). - const globs = Glob.normalize(globList, options.restrictive); - const len = globs.length; - const empty = this._isArray ? [] : {}; - - // if globs is "" or [""] or ["!*"] or ["![*]"] set source to empty and return. - if (len === 0 || (len === 1 && (!globs[0] || re.NEGATE_ALL.test(globs[0])))) { - this._source = empty; - return this; - } - - const cloned = utils.cloneDeep(this.value); - - const firstIsWildcard = re.WILDCARD.test(globs[0]); - // if globs only consist of "*" or "[*]"; set the "clone" as source and - // return. - if (len === 1 && firstIsWildcard) { - this._source = cloned; - return this; - } - - let filtered; - // if the first item of sorted globs is "*" or "[*]" we set the source - // to the (full) "copy" and remove the wildcard from globs (not to - // re-process). - if (firstIsWildcard) { - filtered = new Notation(cloned); - globs.shift(); - } else { - // otherwise we set an empty object or array as the source so that - // we can add notations/properties to it. - filtered = new Notation(empty); - } - - // iterate through globs - utils.each(globs, globNotation => { - // console.log('globNotation', globNotation); - const g = new Glob(globNotation); - const { glob, absGlob, isNegated, levels } = g; - let normalized, emptyValue, eType; - // check whether the glob ends with `.*` or `[*]` then remove - // trailing glob note and decide for empty value (if negated). for - // non-negated, trailing wildcards are already removed by - // normalization. - if (absGlob.slice(-2) === '.*') { - normalized = absGlob.slice(0, -2); - /* istanbul ignore else */ - if (isNegated) emptyValue = {}; - eType = 'object'; - } else if (absGlob.slice(-3) === '[*]') { - normalized = absGlob.slice(0, -3); - /* istanbul ignore else */ - if (isNegated) emptyValue = []; - eType = 'array'; - } else { - normalized = absGlob; - } - - // we'll check glob vs value integrity if emptyValue is set; and throw if needed. - const errGlobIntegrity = `Integrity failed for glob '${glob}'. Cannot set empty ${eType} for '${normalized}' which has a type of `; // ... - - // check if remaining normalized glob has no wildcard stars e.g. - // "a.b" or "!a.b.c" etc.. - if (re.WILDCARDS.test(normalized) === false) { - if (isNegated) { - // inspect and directly remove the notation if negated. - // we need the inspection for the detailed error below. - const insRemove = filtered.inspectRemove(normalized); - // console.log('insRemove', insRemove); - - // if original glob had `.*` at the end, it means remove - // contents (not itself). so we'll set an empty object. - // meaning `some.prop` (prop) is removed completely but - // `some.prop.*` (prop) results in `{}`. For array notation - // (`[*]`), we'll set an empty array. - if (emptyValue) { - // e.g. for glob `![0].x.*` we expect to set `[0].x = {}` - // but if `.x` is not an object (or array), we should fail. - const vType = insRemove.type; - const errMsg = errGlobIntegrity + `'${vType}'.`; - // in non-strict mode, only exceptions are `null` and - // `undefined`, for which we won't throw but we'll not - // set an empty obj/arr either. - - const isValSet = utils.isset(insRemove.value); - // on critical type mismatch we throw - // or if original value is undefined or null in strict mode we throw - if ((isValSet && vType !== eType) || (!isValSet && this.options.strict)) { - throw new NotationError(errMsg); - } - // if parent is an array, we'll insert the value at - // index bec. we've removed the item and indexes are - // shifted. Otherwise, we'll simply overwrite the - // object property value. - const setMode = insRemove.parentIsArray ? 'insert' : 'overwrite'; - // console.log('setting', normalized, emptyValue, setMode); - filtered.set(normalized, emptyValue, setMode); - } - } else { - // directly set the same notation from the original - const insGet = this.inspectGet(normalized); // Notation.create(original).inspectGet ... - /* istanbul ignore else */ - if (insGet.has) filtered.set(normalized, insGet.value, 'overwrite'); - } - // move to the next - return true; - } - - // if glob has wildcard(s), we'll iterate through keys of the source - // object and see if (full) notation of each key matches the current - // glob. - - // important! we will iterate with eachRight to prevent shifted - // indexes when removing items from arrays. - const reverseIterateIfArray = true; - - _each(this._source, (originalNotation, key, value) => { - const originalIsCovered = Glob.create(normalized).covers(originalNotation); - // console.log('» normalized:', normalized, 'covers', originalNotation, '»', originalIsCovered); - if (!originalIsCovered) return true; // break - - if (this.options.strict && emptyValue) { - // since original is covered and we have emptyValue set (due - // to trailing wildcard), here we'll check value vs glob - // integrity; (only if we're in strict mode). - - const vType = utils.type(value); - // types and number of levels are the same? - if (vType !== eType - // we subtract 1 from number of levels bec. the last - // note is removed since we have emptyValue set. - && Notation.split(originalNotation).length === levels.length - 1) { - throw new NotationError(errGlobIntegrity + `'${vType}'.`); - } - } - - // iterating each note of original notation. i.e.: - // note1.note2.note3 is iterated from left to right, as: - // 'note1', 'note1.note2', 'note1.note2.note3' — in order. - Notation.eachNote(originalNotation, levelNotation => { - // console.log(' level »', glob, 'covers', levelNotation, '»', g.test(levelNotation)); - - if (g.test(levelNotation)) { - const levelLen = Notation.split(levelNotation).length; - /* istanbul ignore else */ - if (isNegated && levels.length <= levelLen) { - // console.log(' » removing', levelNotation, 'of', originalNotation); - filtered.remove(levelNotation); - // we break and return early if removed bec. e.g. - // when 'note1.note2' (parent) of - // 'note1.note2.note3' is also removed, we no more - // have 'note3'. - return false; - } - // console.log(' » setting', levelNotation, '=', value); - filtered.set(levelNotation, value, 'overwrite'); - } - }); - }, reverseIterateIfArray); - }); - // finally set the filtered's value as the source of our instance and - // return. - this._source = filtered.value; - return this; - } - - /** - * Removes the property from the source object, at the given notation. - * @alias Notation#delete - * @chainable - * @param {String} notation - The notation to be inspected. - * @returns {Notation} - The current `Notation` instance (self). - * @throws {NotationError} - If `strict` option is enabled and notation - * does not exist. - * - * @example - * const obj = { notebook: "Mac", car: { model: "Mustang" } }; - * Notation.create(obj).remove("car.model"); - * console.log(obj); // { notebook: "Mac", car: { } } - */ - remove(notation) { - const result = this.inspectRemove(notation); - // if strict, check if implied index or prop exists - if (this.options.strict && !result.has) { - const msg = result.parentIsArray ? ERR.NO_INDEX : ERR.NO_PROP; - throw new NotationError(msg + `'${notation}'`); - } - return this; - } - - /** - * Alias of `Notation#remove` - * @private - * @param {String} notation - - * @returns {Notation} - - */ - delete(notation) { - this.remove(notation); - return this; - } - - /** - * Copies the notated property from the source collection and adds it to the - * destination — only if the source object actually has that property. - * This is different than a property with a value of `undefined`. - * @chainable - * - * @param {Object|Array} destination - The destination object that the notated - * properties will be copied to. - * @param {String} notation - The notation to get the corresponding property - * from the source object. - * @param {String} [newNotation=null] - The notation to set the source property - * on the destination object. In other words, the copied property will be - * renamed to this value before set on the destination object. If not set, - * `notation` argument will be used. - * @param {Boolean} [overwrite=true] - Whether to overwrite the property on - * the destination object if it exists. - * - * @returns {Notation} - The current `Notation` instance (self). - * - * @throws {NotationError} - If `destination` is not a valid collection. - * @throws {NotationError} - If `notation` or `newNotation` is invalid. - * - * @example - * const obj = { car: { brand: "Ford", model: "Mustang" } }; - * const models = { dodge: "Charger" }; - * Notation.create(obj).copyTo(models, "car.model", "ford"); - * console.log(models); - * // { dodge: "Charger", ford: "Mustang" } - * // source object (obj) is not modified - */ - copyTo(destination, notation, newNotation = null, overwrite = true) { - if (!utils.isCollection(destination)) throw new NotationError(ERR.DEST); - const result = this.inspectGet(notation); - if (result.has) { - const newN = utils.getNewNotation(newNotation, notation); - Notation.create(destination).set(newN, result.value, overwrite); - } - return this; - } - - /** - * Copies the notated property from the target collection and adds it to - * (own) source object — only if the target object actually has that - * property. This is different than a property with a value of `undefined`. - * @chainable - * - * @param {Object|Array} target - The target collection that the notated - * properties will be copied from. - * @param {String} notation - The notation to get the corresponding - * property from the target object. - * @param {String} [newNotation=null] - The notation to set the copied - * property on our source collection. In other words, the copied property - * will be renamed to this value before set. If not set, `notation` - * argument will be used. - * @param {Boolean} [overwrite=true] - Whether to overwrite the property on - * our collection if it exists. - * - * @returns {Notation} - The current `Notation` instance (self). - * - * @throws {NotationError} - If `target` is not a valid collection. - * @throws {NotationError} - If `notation` or `newNotation` is invalid. - * - * @example - * const obj = { car: { brand: "Ford", model: "Mustang" } }; - * const models = { dodge: "Charger" }; - * Notation.create(obj).copyFrom(models, "dodge", "car.model", true); - * console.log(obj); - * // { car: { brand: "Ford", model: "Charger" } } - * // models object is not modified - */ - copyFrom(target, notation, newNotation = null, overwrite = true) { - if (!utils.isCollection(target)) throw new NotationError(ERR.DEST); - const result = Notation.create(target).inspectGet(notation); - if (result.has) { - const newN = utils.getNewNotation(newNotation, notation); - this.set(newN, result.value, overwrite); - } - return this; - } - - /** - * Removes the notated property from the source (own) collection and adds - * it to the destination — only if the source collection actually has that - * property. This is different than a property with a value of `undefined`. - * @chainable - * - * @param {Object|Array} destination - The destination collection that the - * notated properties will be moved to. - * @param {String} notation - The notation to get the corresponding - * property from the source object. - * @param {String} [newNotation=null] - The notation to set the source - * property on the destination object. In other words, the moved property - * will be renamed to this value before set on the destination object. If - * not set, `notation` argument will be used. - * @param {Boolean} [overwrite=true] - Whether to overwrite the property on - * the destination object if it exists. - * - * @returns {Notation} - The current `Notation` instance (self). - * - * @throws {NotationError} - If `destination` is not a valid collection. - * @throws {NotationError} - If `notation` or `newNotation` is invalid. - * - * @example - * const obj = { car: { brand: "Ford", model: "Mustang" } }; - * const models = { dodge: "Charger" }; - * Notation.create(obj).moveTo(models, "car.model", "ford"); - * console.log(obj); - * // { car: { brand: "Ford" } } - * console.log(models); - * // { dodge: "Charger", ford: "Mustang" } - */ - moveTo(destination, notation, newNotation = null, overwrite = true) { - if (!utils.isCollection(destination)) throw new NotationError(ERR.DEST); - const result = this.inspectRemove(notation); - if (result.has) { - const newN = utils.getNewNotation(newNotation, notation); - Notation.create(destination).set(newN, result.value, overwrite); - } - return this; - } - - /** - * Removes the notated property from the target collection and adds it to (own) - * source collection — only if the target object actually has that property. - * This is different than a property with a value of `undefined`. - * @chainable - * - * @param {Object|Array} target - The target collection that the notated - * properties will be moved from. - * @param {String} notation - The notation to get the corresponding property - * from the target object. - * @param {String} [newNotation=null] - The notation to set the target - * property on the source object. In other words, the moved property - * will be renamed to this value before set on the source object. - * If not set, `notation` argument will be used. - * @param {Boolean} [overwrite=true] - Whether to overwrite the property on - * the source object if it exists. - * - * @returns {Notation} - The current `Notation` instance (self). - * - * @throws {NotationError} - If `target` is not a valid collection. - * @throws {NotationError} - If `notation` or `newNotation` is invalid. - * - * @example - * const obj = { car: { brand: "Ford", model: "Mustang" } }; - * const models = { dodge: "Charger" }; - * Notation.create(obj).moveFrom(models, "dodge", "car.model", true); - * console.log(obj); - * // { car: { brand: "Ford", model: "Charger" } } - * console.log(models); - * // {} - */ - moveFrom(target, notation, newNotation = null, overwrite = true) { - if (!utils.isCollection(target)) throw new NotationError(ERR.DEST); - const result = Notation.create(target).inspectRemove(notation); - if (result.has) { - const newN = utils.getNewNotation(newNotation, notation); - this.set(newN, result.value, overwrite); - } - return this; - } - - /** - * Renames the notated property of the source collection by the new notation. - * @alias Notation#renote - * @chainable - * - * @param {String} notation - The notation to get the corresponding - * property (value) from the source collection. - * @param {String} newNotation - The new notation for the targeted - * property value. If not set, the source collection will not be modified. - * @param {Boolean} [overwrite=true] - Whether to overwrite the property at - * the new notation, if it exists. - * - * @returns {Notation} - The current `Notation` instance (self). - * - * @throws {NotationError} - If `notation` or `newNotation` is invalid. - * - * @example - * const obj = { car: { brand: "Ford", model: "Mustang" } }; - * Notation.create(obj) - * .rename("car.brand", "carBrand") - * .rename("car.model", "carModel"); - * console.log(obj); - * // { carBrand: "Ford", carModel: "Mustang" } - */ - rename(notation, newNotation, overwrite) { - return this.moveTo(this._source, notation, newNotation, overwrite); - } - - /** - * Alias for `#rename` - * @private - * @param {String} notation - - * @param {String} newNotation - - * @param {Boolean} [overwrite=true] - - * @returns {Notation} - - */ - renote(notation, newNotation, overwrite) { - return this.rename(notation, newNotation, overwrite); - } - - /** - * Extracts the property at the given notation to a new object by copying - * it from the source collection. This is equivalent to `.copyTo({}, - * notation, newNotation)`. - * @alias Notation#copyToNew - * - * @param {String} notation - The notation to get the corresponding - * property (value) from the source object. - * @param {String} newNotation - The new notation to be set on the new - * object for the targeted property value. If not set, `notation` argument - * will be used. - * - * @returns {Object} - Returns a new object with the notated property. - * - * @throws {NotationError} - If `notation` or `newNotation` is invalid. - * - * @example - * const obj = { car: { brand: "Ford", model: "Mustang" } }; - * const extracted = Notation.create(obj).extract("car.brand", "carBrand"); - * console.log(extracted); - * // { carBrand: "Ford" } - * // obj is not modified - */ - extract(notation, newNotation) { - const o = {}; - this.copyTo(o, notation, newNotation); - return o; - } - - /** - * Alias for `#extract` - * @private - * @param {String} notation - - * @param {String} newNotation - - * @returns {Object} - - */ - copyToNew(notation, newNotation) { - return this.extract(notation, newNotation); - } - - /** - * Extrudes the property at the given notation to a new collection by - * moving it from the source collection. This is equivalent to `.moveTo({}, - * notation, newNotation)`. - * @alias Notation#moveToNew - * - * @param {String} notation - The notation to get the corresponding - * property (value) from the source object. - * @param {String} newNotation - The new notation to be set on the new - * object for the targeted property value. If not set, `notation` argument - * will be used. - * - * @returns {Object} - Returns a new object with the notated property. - * - * @example - * const obj = { car: { brand: "Ford", model: "Mustang" } }; - * const extruded = Notation.create(obj).extrude("car.brand", "carBrand"); - * console.log(obj); - * // { car: { model: "Mustang" } } - * console.log(extruded); - * // { carBrand: "Ford" } - */ - extrude(notation, newNotation) { - const o = {}; - this.moveTo(o, notation, newNotation); - return o; - } - - /** - * Alias for `#extrude` - * @private - * @param {String} notation - - * @param {String} newNotation - - * @returns {Object} - - */ - moveToNew(notation, newNotation) { - return this.extrude(notation, newNotation); - } - - // -------------------------------- - // STATIC MEMBERS - // -------------------------------- - - /** - * Basically constructs a new `Notation` instance. - * @chainable - * @param {Object|Array} [source={}] - The source collection to be notated. - * @param {Object} [options] - Notation options. - * @param {Boolean} [options.strict=false] - Whether to throw when a - * notation path does not exist on the source. (Note that `.inspectGet()` - * and `.inspectRemove()` methods are exceptions). It's recommended to - * set this to `true` and prevent silent failures if you're working - * with sensitive data. Regardless of `strict` option, it will always - * throw on invalid notation syntax or other crucial failures. - * - * @returns {Notation} - The created instance. - * - * @example - * const obj = { car: { brand: "Dodge", model: "Charger", year: 1970 } }; - * const notation = Notation.create(obj); // equivalent to new Notation(obj) - * notation.get('car.model') // » "Charger" - * notation.remove('car.model').set('car.color', 'red').value - * // » { car: { brand: "Dodge", year: 1970, color: "red" } } - */ - static create(source, options) { - if (arguments.length === 0) { - return new Notation({}); - } - return new Notation(source, options); - } - - /** - * Checks whether the given notation string is valid. Note that the star - * (`*`) (which is a valid character, even if irregular) is NOT treated as - * wildcard here. This checks for regular dot-notation, not a glob-notation. - * For glob notation validation, use `Notation.Glob.isValid()` method. Same - * goes for the negation character/prefix (`!`). - * - * @param {String} notation - The notation string to be checked. - * @returns {Boolean} - - * - * @example - * Notation.isValid('prop1.prop2.prop3'); // true - * Notation.isValid('x'); // true - * Notation.isValid('x.arr[0].y'); // true - * Notation.isValid('x["*"]'); // true - * Notation.isValid('x.*'); // false (this would be valid for Notation#filter() only or Notation.Glob class) - * Notation.isValid('@1'); // false (should be "['@1']") - * Notation.isValid(null); // false - */ - static isValid(notation) { - return typeof notation === 'string' && reVALIDATOR.test(notation); - } - - /** - * Splits the given notation string into its notes (levels). - * @param {String} notation Notation string to be splitted. - * @returns {Array} - A string array of notes (levels). - * @throws {NotationError} - If given notation is invalid. - */ - static split(notation) { - if (!Notation.isValid(notation)) { - throw new NotationError(ERR.NOTATION + `'${notation}'`); - } - return notation.match(reMATCHER); - } - - /** - * Joins the given notes into a notation string. - * @param {String} notes Notes (levels) to be joined. - * @returns {String} Joined notation string. - */ - static join(notes) { - return utils.joinNotes(notes); - } - - /** - * Counts the number of notes/levels in the given notation. - * @alias Notation.countLevels - * @param {String} notation - The notation string to be processed. - * @returns {Number} - Number of notes. - * @throws {NotationError} - If given notation is invalid. - */ - static countNotes(notation) { - return Notation.split(notation).length; - } - - /** - * Alias of `Notation.countNotes`. - * @private - * @param {String} notation - - * @returns {Number} - - */ - static countLevels(notation) { - return Notation.countNotes(notation); - } - - /** - * Gets the first (root) note of the notation string. - * @param {String} notation - The notation string to be processed. - * @returns {String} - First note. - * @throws {NotationError} - If given notation is invalid. - * - * @example - * Notation.first('first.prop2.last'); // "first" - */ - static first(notation) { - return Notation.split(notation)[0]; - } - - /** - * Gets the last note of the notation string. - * @param {String} notation - The notation string to be processed. - * @returns {String} - Last note. - * @throws {NotationError} - If given notation is invalid. - * - * @example - * Notation.last('first.prop2.last'); // "last" - */ - static last(notation) { - const list = Notation.split(notation); - return list[list.length - 1]; - } - - /** - * Gets the parent notation (up to but excluding the last note) - * from the notation string. - * @param {String} notation - The notation string to be processed. - * @returns {String} - Parent note if any. Otherwise, `null`. - * @throws {NotationError} - If given notation is invalid. - * - * @example - * Notation.parent('first.prop2.last'); // "first.prop2" - * Notation.parent('single'); // null - */ - static parent(notation) { - const last = Notation.last(notation); - return notation.slice(0, -last.length).replace(/\.$/, '') || null; - } - - /** - * Iterates through each note/level of the given notation string. - * @alias Notation.eachLevel - * - * @param {String} notation - The notation string to be iterated through. - * @param {Function} callback - The callback function to be invoked on - * each iteration. To break out of the loop, return `false` from within the - * callback. - * Callback signature: `callback(levelNotation, note, index, list) { ... }` - * - * @returns {void} - * @throws {NotationError} - If given notation is invalid. - * - * @example - * const notation = 'first.prop2.last'; - * Notation.eachNote(notation, function (levelNotation, note, index, list) { - * console.log(index, note, levelNotation); - * }); - * // 0 "first" "first" - * // 1 "first.prop2" "prop2" - * // 2 "first.prop2.last" "last" - */ - static eachNote(notation, callback) { - const notes = Notation.split(notation); - const levelNotes = []; - utils.each(notes, (note, index) => { - levelNotes.push(note); - if (callback(Notation.join(levelNotes), note, index, notes) === false) return false; - }, Notation); - } - - /** - * Alias of `Notation.eachNote`. - * @private - * @param {String} notation - - * @param {Function} callback - - * @returns {void} - */ - static eachLevel(notation, callback) { - Notation.eachNote(notation, callback); - } - -} - -/** - * Error class specific to `Notation`. - * @private - * - * @class - * @see `{@link #Notation.Error}` - */ -Notation.Error = NotationError; - -/** - * Utility for validating, comparing and sorting dot-notation globs. - * This is internally used by `Notation` class. - * @private - * - * @class - * @see `{@link #Notation.Glob}` - */ -Notation.Glob = Glob; - -/** - * Undocumented - * @private - */ -Notation.utils = utils; - -// -------------------------------- -// HELPERS -// -------------------------------- - -/** - * Deep iterates through each note (level) of each item in the given - * collection. - * @private - * @param {Object|Array} collection A data object or an array, as the source. - * @param {Function} callback A function to be executed on each iteration, - * with the following arguments: `(levelNotation, note, value, collection)` - * @param {Boolean} [reverseIfArray=false] Set to `true` to iterate with - * `eachRight` to prevent shifted indexes when removing items from arrays. - * @param {Boolean} [byLevel=false] Indicates whether to iterate notations by - * each level or by the end value. For example, if we have a collection of - * `{a: { b: true } }`, and `byLevel` is set; the callback will be invoked on - * the following notations: `a`, `a.b`. Otherwise, it will be invoked only on - * `a.b`. - * @param {String} [parentNotation] Storage for parent (previous) notation. - * @param {Collection} [topSource] Storage for initial/main collection. - * @returns {void} - */ -function _each(collection, callback, reverseIfArray = false, byLevel = false, parentNotation = null, topSource = null) { // eslint-disable-line max-params - const source = topSource || collection; - // if (!utils.isCollection(collection)) throw ... // no need - utils.eachItem(collection, (value, keyOrIndex) => { - const note = typeof keyOrIndex === 'number' - ? `[${keyOrIndex}]` - : keyOrIndex; - const currentNotation = Notation.join([parentNotation, note]); - const isCollection = utils.isCollection(value); - // if it's not a collection we'll execute the callback. if it's a - // collection but byLevel is set, we'll also execute the callback. - if (!isCollection || byLevel) { - if (callback(currentNotation, note, value, source) === false) return false; - } - // deep iterating if collection - if (isCollection) _each(value, callback, reverseIfArray, byLevel, currentNotation, source); - }, null, reverseIfArray); -} - -// -------------------------------- -// EXPORT -// -------------------------------- - -export { Notation }; diff --git a/src/index.js b/src/index.js deleted file mode 100644 index a535666..0000000 --- a/src/index.js +++ /dev/null @@ -1,2 +0,0 @@ -/* istanbul ignore file */ -export * from './core/notation'; diff --git a/src/utils.js b/src/utils.js deleted file mode 100644 index 47e81a0..0000000 --- a/src/utils.js +++ /dev/null @@ -1,214 +0,0 @@ - -import { NotationError } from './core/notation.error'; - -const objProto = Object.prototype; -const symValueOf = typeof Symbol === 'function' - ? Symbol.prototype.valueOf - /* istanbul ignore next */ - : null; - -// never use 'g' (global) flag in regexps below -const VAR = /^[a-z$_][a-z$_\d]*$/i; -const ARRAY_NOTE = /^\[(\d+)\]$/; -const ARRAY_GLOB_NOTE = /^\[(\d+|\*)\]$/; -const OBJECT_BRACKETS = /^\[(?:'(.*)'|"(.*)"|`(.*)`)\]$/; -const WILDCARD = /^(\[\*\]|\*)$/; -// matches `*` and `[*]` if outside of quotes. -const WILDCARDS = /(\*|\[\*\])(?=(?:[^"]|"[^"]*")*$)(?=(?:[^']|'[^']*')*$)/; -// matches trailing wildcards at the end of a non-negated glob. -// e.g. `x.y.*[*].*` » $1 = `x.y`, $2 = `.*[*].*` -const NON_NEG_WILDCARD_TRAIL = /^(?!!)(.+?)(\.\*|\[\*\])+$/; -const NEGATE_ALL = /^!(\*|\[\*\])$/; -// ending with '.*' or '[*]' - -const _reFlags = /\w*$/; - -const utils = { - - re: { - VAR, - ARRAY_NOTE, - ARRAY_GLOB_NOTE, - OBJECT_BRACKETS, - WILDCARD, - WILDCARDS, - NON_NEG_WILDCARD_TRAIL, - NEGATE_ALL - }, - - type(o) { - return objProto.toString.call(o).match(/\s(\w+)/i)[1].toLowerCase(); - }, - - isCollection(o) { - const t = utils.type(o); - return t === 'object' || t === 'array'; - }, - - isset(o) { - return o !== undefined && o !== null; - }, - - ensureArray(o) { - if (utils.type(o) === 'array') return o; - return o === null || o === undefined ? [] : [o]; - }, - - // simply returning true will get rid of the "holes" in the array. - // e.g. [0, , 1, , undefined, , , 2, , , null].filter(() => true); - // ——» [0, 1, undefined, 2, null] - - // cleanSparseArray(a) { - // return a.filter(() => true); - // }, - - // added _collectionType for optimization (in loops) - hasOwn(collection, keyOrIndex, _collectionType) { - if (!collection) return false; - const isArr = (_collectionType || utils.type(collection)) === 'array'; - if (!isArr && typeof keyOrIndex === 'string') { - return keyOrIndex && objProto.hasOwnProperty.call(collection, keyOrIndex); - } - if (typeof keyOrIndex === 'number') { - return keyOrIndex >= 0 && keyOrIndex < collection.length; - } - return false; - }, - - cloneDeep(collection) { - const t = utils.type(collection); - switch (t) { - case 'date': - return new Date(collection.valueOf()); - case 'regexp': { - const flags = _reFlags.exec(collection).toString(); - const copy = new collection.constructor(collection.source, flags); - copy.lastIndex = collection.lastIndex; - return copy; - } - case 'symbol': - return symValueOf - ? Object(symValueOf.call(collection)) - /* istanbul ignore next */ - : collection; - case 'array': - return collection.map(utils.cloneDeep); - case 'object': { - const copy = {}; - // only enumerable string keys - Object.keys(collection).forEach(k => { - copy[k] = utils.cloneDeep(collection[k]); - }); - return copy; - } - // primitives copied over by value - // case 'string': - // case 'number': - // case 'boolean': - // case 'null': - // case 'undefined': - default: // others will be referenced - return collection; - } - }, - - // iterates over elements of an array, executing the callback for each - // element. - each(array, callback, thisArg) { - const len = array.length; - let index = -1; - while (++index < len) { - if (callback.apply(thisArg, [array[index], index, array]) === false) return; - } - }, - - eachRight(array, callback, thisArg) { - let index = array.length; - while (index--) { - if (callback.apply(thisArg, [array[index], index, array]) === false) return; - } - }, - - eachProp(object, callback, thisArg) { - const keys = Object.keys(object); - let index = -1; - while (++index < keys.length) { - const key = keys[index]; - if (callback.apply(thisArg, [object[key], key, object]) === false) return; - } - }, - - eachItem(collection, callback, thisArg, reverseIfArray = false) { - if (utils.type(collection) === 'array') { - // important! we should iterate with eachRight to prevent shifted - // indexes when removing items from arrays. - return reverseIfArray - ? utils.eachRight(collection, callback, thisArg) - : utils.each(collection, callback, thisArg); - } - return utils.eachProp(collection, callback, thisArg); - }, - - pregQuote(str) { - const re = /[.\\+*?[^\]$(){}=!<>|:-]/g; - return String(str).replace(re, '\\$&'); - }, - - stringOrArrayOf(o, value) { - return typeof value === 'string' - && (o === value - || (utils.type(o) === 'array' && o.length === 1 && o[0] === value) - ); - }, - - hasSingleItemOf(arr, itemValue) { - return arr.length === 1 - && (arguments.length === 2 ? arr[0] === itemValue : true); - }, - - // remove trailing/redundant wildcards if not negated - removeTrailingWildcards(glob) { - // return glob.replace(/(.+?)(\.\*|\[\*\])*$/, '$1'); - return glob.replace(NON_NEG_WILDCARD_TRAIL, '$1'); - }, - - normalizeNote(note) { - if (VAR.test(note)) return note; - // check array index notation e.g. `[1]` - let m = note.match(ARRAY_NOTE); - if (m) return parseInt(m[1], 10); - // check object bracket notation e.g. `["a-b"]` - m = note.match(OBJECT_BRACKETS); - if (m) return (m[1] || m[2] || m[3]); - throw new NotationError(`Invalid note: '${note}'`); - }, - - joinNotes(notes) { - const lastIndex = notes.length - 1; - return notes.map((current, i) => { - if (!current) return ''; - const next = lastIndex >= i + 1 ? notes[i + 1] : null; - const dot = next - ? next[0] === '[' ? '' : '.' - : ''; - return current + dot; - }).join(''); - }, - - getNewNotation(newNotation, notation) { - const errMsg = `Invalid new notation: '${newNotation}'`; - // note validations (for newNotation and notation) are already made by - // other methods in the flow. - let newN; - if (typeof newNotation === 'string') { - newN = newNotation.trim(); - if (!newN) throw new NotationError(errMsg); - return newN; - } - if (notation && !utils.isset(newNotation)) return notation; - throw new NotationError(errMsg); - } - -}; - -export { utils }; diff --git a/test/ac.spec.js b/test/ac.spec.js deleted file mode 100644 index a307810..0000000 --- a/test/ac.spec.js +++ /dev/null @@ -1,83 +0,0 @@ -/* eslint camelcase:0, consistent-return:0, max-lines-per-function:0 */ - -import { Notation } from '../src/core/notation'; -const notate = Notation.create; -const { union } = Notation.Glob; - -describe('ac', () => { - - test('#filter()', () => { - let globs = ['*', '!account.balance.credit', '!account.id', '!secret']; - let data = { - name: 'Company, LTD.', - address: { - city: 'istanbul', - country: 'TR' - }, - account: { - id: 33, - taxNo: 12345, - balance: { - credit: 100, - deposit: 0 - } - }, - secret: { - value: 'hidden' - } - }; - let filtered = notate(data).filter(globs).value; - expect(filtered.name).toEqual(expect.any(String)); - expect(filtered.address).toEqual(expect.any(Object)); - expect(filtered.address.city).toEqual('istanbul'); - expect(filtered.account).toBeDefined(); - expect(filtered.account.id).toBeUndefined(); - expect(filtered.account.balance).toBeDefined(); - expect(filtered.account.credit).toBeUndefined(); - expect(filtered.secret).toBeUndefined(); - - filtered = notate(data).filter('!*').value; - expect(filtered).toEqual({}); - - // filtering array of objects - globs = ['*', '!id']; - data = [ - { id: 1, name: 'x', age: 30 }, - { id: 2, name: 'y', age: 31 }, - { id: 3, name: 'z', age: 32 } - ]; - filtered = notate(data).filter(globs).value; - expect(filtered).toEqual(expect.any(Array)); - expect(filtered.length).toEqual(data.length); - }); - - test('#filter() 2', () => { - const o = { - name: 'John', - age: 30, - account: { - id: 1, - country: 'US' - } - }; - const filtered = notate(o).filter(['*', '!account.id', '!age']).value; - expect(filtered.name).toEqual('John'); - expect(filtered.account.id).toBeUndefined(); - expect(filtered.account.country).toEqual('US'); - - expect(o.account.id).toEqual(1); - expect(o).not.toEqual(filtered); - }); - - test('#union()', () => { - const globA = ['*', '!id', '!pwd']; - const globB = ['*', '!pwd', 'title']; - expect(union(globA, globB)).toEqual(['*', '!pwd']); - - let u = union(['image', 'name'], ['name', '!location']); - u = union(u, ['*', '!location']); - expect(u).toEqual(['*', '!location']); - u = union(['*'], u); - expect(u).toEqual(['*']); - }); -}); diff --git a/test/filter.spec.js b/test/filter.spec.js deleted file mode 100644 index 3c10a29..0000000 --- a/test/filter.spec.js +++ /dev/null @@ -1,409 +0,0 @@ -/* eslint camelcase:0, max-lines-per-function:0, consistent-return:0, max-statements:0, max-lines:0, max-len:0 */ - -import * as _ from 'lodash'; -import { Notation } from '../src/core/notation'; -import { utils } from '../src/utils'; - -const notate = Notation.create; - -const o = { - name: 'onur', - age: 36, - account: { - id: 15, - tags: 20, - likes: ['movies', '3d', 'programming', 'music'] - }, - billing: { - account: { - id: 121, - credit: 300, - balance: -293 - } - }, - company: { - name: 'pilot co', - address: { - city: 'istanbul', - country: 'TR', - location: { - lat: 34.123123, - lon: 30.123123 - } - }, - account: { - id: 33, - taxNo: 12345 - }, - limited: true, - notDefined: undefined, - nuller: null, - zero: 0 - } -}; - - -describe('Notation#filter()', () => { - - test('#filter()', () => { - // var glob = create; - const nota = notate(o).clone(); - // console.log('value ---:', nota.value); - const globs = ['!company.limited', 'billing.account.credit', 'company.*', 'account.id']; - const filtered = nota.filter(globs).value; - // console.log('filtered ---:', filtered); - - expect(filtered.company.name).toBeDefined(); - expect(filtered.company.address).toBeDefined(); - expect(filtered.company.limited).toBeUndefined(); - expect(filtered.account.id).toBeDefined(); - expect(filtered.account.likes).toBeUndefined(); - expect(filtered.billing.account.credit).toBeDefined(); - - // original object should not be modified - // console.log(JSON.stringify(o, null, ' ')); - expect(o.company.name).toBeDefined(); - expect(o.company.limited).toBeDefined(); - expect(o.account.id).toBeDefined(); - - const assets = { model: 'Onur', phone: { brand: 'Apple', model: 'iPhone' }, car: { brand: 'Ford', model: 'Mustang' } }; - const n = notate(assets); - const m1 = n.filter('*').value; - const m2 = n.filter('*.*').value; - const m3 = n.filter('*.*.*').value; - expect(m1.model).toBeDefined(); - expect(m1.phone.model).toBeDefined(); - expect(m2.model).toBeDefined(); - expect(m2.phone.model).toBeDefined(); - expect(m3.model).toBeDefined(); - expect(m3.phone.model).toBeDefined(); - expect(Object.keys(m3).length).toEqual(3); - - // globs.push('*'); - // nota.filter(globs); - }); - - test('#filter() » 2nd level wildcard', () => { - const data = { model: 'Onur', phone: { brand: 'Apple', model: 'iPhone' }, car: { brand: 'Ford', model: 'Mustang' } }; - expect(notate(data).filter('phone.*').value).toEqual({ phone: data.phone }); - }); - - test('#filter() » negated object', () => { - const data = { name: 'Onur', phone: { brand: 'Apple', model: 'iPhone' }, car: { brand: 'Ford', model: 'Mustang' } }; - const globs = ['*', '!phone']; - const filtered = notate(data).filter(globs).value; - expect(filtered.name).toEqual(data.name); - expect(filtered.phone).toBeUndefined(); - expect(filtered.car).toBeDefined(); - // console.log(filtered); - // console.log(data); - }); - - test('#filter() » normal and negated of the same (negated should win)', () => { - const data = { prop: { id: 1, x: true }, y: true }; - // we have the same glob both as negated and normal. negated should win. - let globs = ['prop.id', '!prop.id']; - let filtered = notate(data).filter(globs).value; - expect(filtered.prop).toBeUndefined(); - expect(filtered.y).toBeUndefined(); - // add wildcard - globs = ['!prop.id', 'prop.id', '*']; - filtered = notate(data).filter(globs).value; - expect(filtered.prop).toEqual(jasmine.any(Object)); - expect(filtered.prop.id).toBeUndefined(); - expect(filtered.prop.x).toEqual(true); - expect(filtered.y).toEqual(data.y); - }); - - test('#filter() » with/out wildcard', () => { - const data = { name: 'Onur', id: 1 }; - // we have no wildcard '*' here. - let globs = ['!id']; - // should filter as `{}` - let filtered = notate(data).filter(globs).value; - expect(filtered).toEqual(jasmine.any(Object)); - expect(Object.keys(filtered).length).toEqual(0); - // add wildcard - globs = ['*', '!id']; - filtered = notate(data).filter(globs).value; - expect(filtered.name).toEqual(data.name); - expect(filtered.id).toBeUndefined(); - // no negated (id is duplicate in this case) - globs = ['*', 'id']; - filtered = notate(data).filter(globs).value; - expect(filtered.name).toEqual(data.name); - expect(filtered.id).toEqual(data.id); - }); - - test('#filter() » wildcards', () => { - const data = { - x: { - y: { z: 1 }, - a: { b: 2 } - }, - c: 3, - d: { - e: { f: 4, g: { i: 5 } } - } - }; - - function filter(globs) { - return notate(_.cloneDeep(data)).filter(globs); - } - let result; - - function check1() { - expect(result.x.y.z).toEqual(1); - expect(result.x.a.b).toEqual(2); - expect(result.c).toBeUndefined(); - expect(result.d).toBeUndefined(); - } - - // these should be treated the same: - // 'x.*.*' === 'x.*' === 'x' - - result = filter(['x.*.*']).value; - check1(); - result = filter(['x.*']).value; - check1(); - result = filter(['x']).value; - check1(); - - // these should NOT be treated the same: - // '!x.*.*' !== '!x.*' !== '!x' - - result = filter(['*', '!x.*.*']).value; - // console.log(result); - expect(result.x).toEqual(jasmine.any(Object)); - expect(result.x.y).toEqual(jasmine.any(Object)); - expect(result.x.a).toEqual(jasmine.any(Object)); - expect(Object.keys(result.x.y).length).toEqual(0); - expect(Object.keys(result.x.a).length).toEqual(0); - expect(result.c).toEqual(3); - expect(result.d).toEqual(jasmine.any(Object)); - - result = filter(['*', '!x.*']).value; - expect(result.x).toEqual(jasmine.any(Object)); - expect(Object.keys(result.x).length).toEqual(0); - expect(result.c).toEqual(3); - expect(result.d).toEqual(jasmine.any(Object)); - - result = filter(['*', '!x']).value; - // console.log('!x\t', result); - expect(result.x).toBeUndefined(); - expect(result.c).toEqual(3); - expect(result.d).toEqual(jasmine.any(Object)); - - result = filter(['*']).value; - // expect(JSON.stringify(result)).toEqual(JSON.stringify(data)); - expect(result).toEqual(data); - // console.log('*\t', result); - - result = filter(['*', '!*']).value; - expect(result).toEqual({}); - - // console.log(JSON.stringify(data, null, ' ')); - - result = filter(['*', '!*.*.*']).value; - // console.log('!*.*.*\n', JSON.stringify(result, null, ' ')); - expect(result.x).toEqual(jasmine.any(Object)); - expect(result.x.y).toEqual(jasmine.any(Object)); - expect(Object.keys(result.x.y).length).toEqual(0); - expect(result.x.a).toEqual(jasmine.any(Object)); - expect(Object.keys(result.x.a).length).toEqual(0); - expect(result.d.e).toEqual(jasmine.any(Object)); - expect(Object.keys(result.d.e).length).toEqual(0); - expect(result.c).toEqual(3); - - result = filter(['*', '!*.*']).value; - // console.log('!*.*\n', JSON.stringify(result, null, ' ')); - expect(result.x).toEqual(jasmine.any(Object)); - expect(Object.keys(result.x).length).toEqual(0); - expect(result.d).toEqual(jasmine.any(Object)); - expect(Object.keys(result.d).length).toEqual(0); - expect(result.c).toEqual(3); - }); - - test('#filter() » other', () => { - const globs = ['*', '!box', 'box.model.*', '!bValid.*', '!sto.p2m', '!sto.contact.*', '!sto.partners.*', '!sto.powOp.*']; - const data = { - id: 'TR001', - box: { - model: { code: 'N22', description: 'Normal 22 kVA' }, - supplier: 'EFA', - router: { brandModel: 'TELT 104' }, - connection: { operator: 'VODA', method: '3G' } - }, - bValid: { - method: 'OP', - comment: '', - date: 'Mon Nov 18 2013 15:41:43 GMT+0200 (EET)' - } - }; - const originalClone = utils.cloneDeep(data); - - const filtered = notate(data).filter(globs).value; - expect(filtered.box.model).toBeDefined(); - expect(filtered.box.router).toBeUndefined(); // "!box" - expect(filtered.bValid).toEqual({}); // "!validation.*" - expect(data).toEqual(originalClone); - }); - - test('#filter() » bracket', () => { - expect(notate([{ x: 1 }]).filter(['*']).value).toEqual([{ x: 1 }]); - expect(notate([{ x: 1 }]).filter(['[*]']).value).toEqual([{ x: 1 }]); - - const obj = [ - { x: 1, y: [2, 3, { z: 4 }] }, - [1, 2, 3], - { 'my-prop': [4, 5] } - ]; - - // [0].x.* is invalid since x is not an object but a number - expect(() => notate(obj).filter(['[*]', '![0].x.*'])).toThrow(); - - const globs = ['[*]', '![0].x', '![0].y[1]', '![0].y[2].z', '![2]["my-prop"][0]']; - const filtered = notate(obj).filter(globs).value; - - expect(filtered[0].y[1]).toEqual({ z: 4 }); - expect(filtered[0].x).toBeUndefined(); - expect(filtered[1]).toEqual([1, 2, 3]); - expect(filtered[2]['my-prop']).toEqual([5]); - }); - - test('#filter() » bracket 2', () => { - let filtered = null; - - let obj = [ - [1, 2, 3], - [4, 5, [[6]]], - [7, 8] - ]; - - let globs = ['[*]', '![*][1]', '![0][1]', '![0][2]', '![1][2][0]']; - filtered = notate(obj).filter(globs).value; - expect(filtered[0].length).toEqual(1); - - filtered = notate([0, 1, 2]).filter(['[*]', '![2]', '![0]']).value; - expect(filtered).toEqual([1]); - - filtered = notate([0, 1, [2, 3], 4, 5]).filter(['[*]', '![2][*]']).value; - expect(filtered).toEqual([0, 1, [], 4, 5]); - - filtered = notate([0, 1, [2, 3, 4], 5, 6]).filter(['[*]', '![2][1]']).value; - expect(filtered).toEqual([0, 1, [2, 4], 5, 6]); - - filtered = notate(filtered).filter(['[*]', '![1]', '![4]']).value; - expect(filtered).toEqual([0, [2, 4], 5]); - - filtered = notate([0, 1, [2, 3, 4], 5, [6, 7, 8], 9]).filter(['[*]', '![*][1]']).value; - expect(filtered).toEqual([0, 1, [2, 4], 5, [6, 8], 9]); - - filtered = notate([0, 1, [2, 3, 4, 1], 5, [3, 6, 7, 8], 9]).filter(['[*]', '![*][*]']).value; - expect(filtered).toEqual([0, 1, [], 5, [], 9]); - - filtered = notate([0, 1, [2, 3, 4], 5, [{ x: 1 }], [6, 7], [8, [9, 10]]]).filter(['[*]', '![*][*]']).value; - expect(filtered).toEqual([0, 1, [], 5, [], [], []]); - - filtered = notate([0, 1, [2, [3]], 4]).filter(['[*]', '![2][1][*]']).value; - expect(filtered).toEqual([0, 1, [2, []], 4]); - - filtered = notate([0, 1, [2, [3, [5], 6]], 4]).filter(['[*]', '![2][*][1][*]']).value; - expect(filtered).toEqual([0, 1, [2, [3, [], 6]], 4]); - - filtered = notate([0, 1, [2, [[null], [5], 6]], [4]]).filter(['[*]', '![2][*][*][*]']).value; - expect(filtered).toEqual([0, 1, [2, [[], [], 6]], [4]]); - - filtered = notate([0, 1, [2], [null], null, [undefined], undefined]).filter(['[*]', '![*][*]']).value; - expect(filtered).toEqual([0, 1, [], [], null, [], undefined]); - - filtered = notate([{ x: [3] }]).filter(['[*]', '![0].x[*]']).value; - expect(filtered).toEqual([{ x: [] }]); - - filtered = notate([{ x: [{ y: 1 }, { z: 2, y: 3 }] }]).filter(['[*]', '![0].x[*].y']).value; - expect(filtered).toEqual([{ x: [{}, { z: 2 }] }]); - - obj = [{ x: [{ y: 1 }, { z: 2, y: 3 }] }]; - globs = ['[*]', '![0].x[*].y.*']; - // expect no change bec. no y is object - expect(notate(obj).filter(globs).value).toEqual(obj); - // same should throw in strict mode - filtered = () => notate(obj, { strict: true }).filter(globs).value; - expect(filtered).toThrow(); - - obj = [{ x: [{ y: { a: 1 } }, { z: 2, y: { b: 3, c: 4 } }] }]; - filtered = notate(obj, { strict: true }).filter(globs).value; - expect(filtered).toEqual([{ x: [{ y: {} }, { z: 2, y: {} }] }]); - - obj = [{ x: [{ y: [1] }, { z: 2, y: [3, 4] }] }]; - filtered = notate(obj).filter(globs).value; - // expect no change bec. no y is object - expect(filtered).toEqual(obj); - - obj = [{ x: [{ y: [1] }, { z: 2, y: [3, 4] }] }]; - filtered = notate(obj).clone().filter(globs).value; - // expect no change bec. no y is object - expect(filtered).toEqual(obj); - - globs = ['[*]', '![0].x[*].y[*]']; - filtered = notate(obj).clone().filter(globs).value; - expect(filtered).toEqual([{ x: [{ y: [] }, { z: 2, y: [] }] }]); - - filtered = notate({ x: null }).filter(['*', '!x[*]']).value; - expect(filtered).toEqual({ x: [] }); - filtered = notate({ x: undefined }).filter(['*', '!x[*]']).value; - expect(filtered).toEqual({ x: [] }); - - // the glob wildcard determines the empty value ({} or []) in non-strict - // mode - filtered = notate({ x: null }).filter(['*', '!x.*']).value; - expect(filtered).toEqual({ x: {} }); - filtered = notate({ x: undefined }).filter(['*', '!x.*']).value; - expect(filtered).toEqual({ x: {} }); - - // should throw for null & undefined in strict mode - filtered = () => notate({ x: null }, { strict: true }).filter(['*', '!x[*]']).value; - expect(filtered).toThrow(); - filtered = () => notate({ x: undefined }, { strict: true }).filter(['*', '!x[*]']).value; - expect(filtered).toThrow(); - filtered = () => notate({ x: null }, { strict: true }).filter(['*', '!x.*']).value; - expect(filtered).toThrow(); - filtered = () => notate({ x: undefined }, { strict: true }).filter(['*', '!x.*']).value; - expect(filtered).toThrow(); - - // should always throw on critical type-mismatch (regardless of strict mode) - filtered = () => notate({ x: 'string' }).filter(['*', '!x.*']).value; - expect(filtered).toThrow(); - filtered = () => notate({ x: true }).filter(['*', '!x[*]']).value; - expect(filtered).toThrow(); - }); - - test('#filter() » bracket 3', () => { - let obj = { - a: { x: 1, y: 2 }, - b: { x: 3, y: 4 } - }; - let filtered; - - const expected = { a: { x: 1 }, b: { x: 3 } }; - filtered = notate(obj).filter('*.x').value; - expect(filtered).toEqual(expected); - filtered = notate(obj).filter('*["x"]').value; - expect(filtered).toEqual(expected); - filtered = notate(obj).filter("*['x']").value; - expect(filtered).toEqual(expected); - - obj = { 'x.y': { z: 1 }, 'x': { y: { z: 2 } } }; - filtered = notate(obj).filter('["x.y"].z').value; - expect(filtered).toEqual({ 'x.y': { z: 1 } }); - const nota = notate(obj); - expect(nota.filter('x.y.z').value).toEqual({ x: { y: { z: 2 } } }); - expect(nota.filter('x.y.z').value).toEqual(nota.filter('x').value); - - obj = { 'a': [1], 'b': 2, 'c': [3, 4], 'd.e': 5 }; - filtered = notate(obj).filter(['c[1]', '["d.e"]', 'a[*]']).value; - // c[1] will create sparse array which is normal - expect(filtered).toEqual({ 'a': [1], 'c': [undefined, 4], 'd.e': 5 }); - }); - -}); diff --git a/test/notation.glob.spec.js b/test/notation.glob.spec.js deleted file mode 100644 index 51485aa..0000000 --- a/test/notation.glob.spec.js +++ /dev/null @@ -1,963 +0,0 @@ -/* eslint camelcase:0, max-lines-per-function:0, consistent-return:0, max-statements:0, max-lines:0, max-len:0 */ - -import { Notation } from '../src/core/notation'; - -// shuffle array -function shuffle(o) { // v1.0 - for (let j, x, i = o.length; i; j = Math.floor(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x); - return o; -} - -describe('Notation.Glob', () => { - - const { - isValid, hasMagic, toRegExp, _inspect, _covers, _intersect, split, compare, sort, normalize, union, create - } = Notation.Glob; - - const reVAR = '[a-z$_][a-z$_\\d]*'; - const reARRINDEX = '\\[\\d+\\]'; - const reREST = '(?:[\\[\\.].+|$)'; - - test('.isValid()', () => { - expect(isValid('prop.mid.last')).toEqual(true); - expect(isValid('prop.*.')).toEqual(false); - expect(isValid('prop.*')).toEqual(true); - expect(isValid('prop.')).toEqual(false); - expect(isValid('prop')).toEqual(true); - expect(isValid('pro*')).toEqual(false); - expect(isValid('.prop')).toEqual(false); - expect(isValid('.')).toEqual(false); - expect(isValid()).toEqual(false); - expect(isValid(1)).toEqual(false); - expect(isValid(null)).toEqual(false); - expect(isValid(true)).toEqual(false); - expect(isValid('')).toEqual(false); - expect(isValid('*.')).toEqual(false); - expect(isValid('*')).toEqual(true); - expect(isValid('.*')).toEqual(false); - expect(isValid('***')).toEqual(false); - expect(isValid('!*')).toEqual(true); - expect(isValid('!**')).toEqual(false); - expect(isValid('!*.*')).toEqual(true); - expect(isValid('!*.**')).toEqual(false); - expect(isValid('!*.*.xxx')).toEqual(true); - - expect(isValid('')).toEqual(false); - expect(isValid('!')).toEqual(false); - expect(isValid('*')).toEqual(true); - expect(isValid('[*]')).toEqual(true); - expect(isValid('["*"]')).toEqual(true); // not wildcard but valid - expect(isValid('["[*]"]')).toEqual(true); // not wildcard but valid - expect(isValid('[]')).toEqual(false); - expect(isValid('[!]')).toEqual(false); - expect(isValid('a[0][1][2].*')).toEqual(true); - expect(isValid('a.b[*].x')).toEqual(true); - expect(isValid('a.b[3]')).toEqual(true); - expect(isValid('a.*[*]')).toEqual(true); - expect(isValid('a.*[*].*')).toEqual(true); - expect(isValid('a.*[1]')).toEqual(true); - expect(isValid('a.*.c[1]')).toEqual(true); - expect(isValid('!a.*.c[1]')).toEqual(true); - expect(isValid('a.*[*][0].c')).toEqual(true); - expect(isValid('!*.*[*][*].*')).toEqual(true); - expect(isValid('x.*["*"][*][1].*["*.x"]')).toEqual(true); - expect(isValid('*.[*]')).toEqual(false); - expect(isValid('*[*]*[*]')).toEqual(false); - expect(isValid('*[*]*.[*]')).toEqual(false); - }); - - test('.hasMagic()', () => { - expect(hasMagic('x.y.z')).toEqual(false); - expect(hasMagic('!x.y.z')).toEqual(true); - expect(hasMagic('x.y.!z')).toEqual(false); // invalid - expect(hasMagic('[*]')).toEqual(true); - expect(hasMagic('x.*')).toEqual(true); - expect(hasMagic('[*].x.y.*')).toEqual(true); - expect(hasMagic('')).toEqual(false); - expect(hasMagic(null)).toEqual(false); // invalid - expect(hasMagic(true)).toEqual(false); // invalid - expect(hasMagic('*.')).toEqual(false); // invalid - }); - - test('.toRegExp()', () => { - expect(() => toRegExp().source).toThrow(); - expect(() => toRegExp('').source).toThrow(); - expect(() => toRegExp('*.').source).toThrow(); - expect(toRegExp('*').source).toEqual('^' + reVAR + reREST); - expect(toRegExp('[*]').source).toEqual('^' + reARRINDEX + reREST); - expect(toRegExp('["*"]').source).toEqual('^\\["\\*"\\]' + reREST); - expect(toRegExp('x.*').source).toEqual('^x\\.' + reVAR + reREST); - expect(toRegExp('!x.*["x.*"][1]').source).toEqual('^x\\.' + reVAR + '\\["x\\.\\*"\\]\\[1\\]' + reREST); - }); - - test('._inspect()', () => { - let ins = _inspect('![*].x.y[1].*[*]'); - expect(ins.glob).toEqual('![*].x.y[1].*[*]'); - expect(ins.absGlob).toEqual('[*].x.y[1].*[*]'); // 'cause original is negated - expect(ins.isNegated).toEqual(true); - - ins = _inspect('*.x[*].y'); - expect(ins.glob).toEqual('*.x[*].y'); - expect(ins.absGlob).toEqual('*.x[*].y'); - expect(ins.isNegated).toEqual(false); - }); - - test('.split()', () => { - expect(() => split()).toThrow(); - expect(() => split('')).toThrow(); - expect(() => split('.x')).toThrow(); - expect(() => split('[x]')).toThrow(); - expect(split('x')).toEqual(['x']); - expect(split('*')).toEqual(['*']); - expect(split('[*]')).toEqual(['[*]']); - expect(split('["[*]"]')).toEqual(['["[*]"]']); - expect(split('!["ab.c"]')).toEqual(['["ab.c"]']); - expect(split('*.x.y[*][1].z["*.x"].a')).toEqual(['*', 'x', 'y', '[*]', '[1]', 'z', '["*.x"]', 'a']); - expect(split('*.*[*]')).toEqual(['*', '*', '[*]']); - // normalized/cleaned - expect(split('*.*[*]', true)).toEqual(['*']); - expect(split('[*].*[*].*', true)).toEqual(['[*]']); - expect(split('x.y[*]', true)).toEqual(['x', 'y']); - expect(split('!x.y[*]', true)).toEqual(['x', 'y', '[*]']); - expect(split('x.y.*.z', true)).toEqual(['x', 'y', '*', 'z']); - expect(split('x.y[*].z.*', true)).toEqual(['x', 'y', '[*]', 'z']); - }); - - test('.compare()', () => { - expect(compare('*', 'a')).toEqual(-1); - expect(compare('a', '*')).toEqual(1); - expect(compare('!*', 'a')).toEqual(-1); - expect(compare('*', '*')).toEqual(0); - expect(compare('*', '[*]')).toEqual(0); - expect(compare('[*]', '*')).toEqual(0); - expect(compare('[*]', '[*]')).toEqual(0); - expect(compare('[*]', 'a')).toEqual(-1); - expect(compare('a', '[*]')).toEqual(1); - expect(compare('*.*', '*')).toEqual(0); - expect(compare('*.*.*', '*')).toEqual(0); - expect(compare('*', '*[*].*')).toEqual(0); - expect(compare('*.*', '*.a')).toEqual(-1); - expect(compare('*', '!*.*')).toEqual(-1); - expect(compare('*.x', 'y.*')).toEqual(1); - expect(compare('[*].x', 'a.*')).toEqual(1); - expect(compare('*.x.*', 'x.*')).toEqual(1); - expect(compare('*.x.y', 'x.y')).toEqual(1); - - expect(compare('a.*', 'a.b[1]')).toEqual(-1); - expect(compare('a.b', 'a[*]')).toEqual(1); - expect(compare('a.b[3]', 'a.b[2]')).toEqual(1); - expect(compare('a.*[*]', 'a.*')).toEqual(0); - expect(compare('a', 'a[*]')).toEqual(0); - expect(compare('a.*[1]', 'a.*[*]')).toEqual(1); - expect(compare('a.*.c[1]', 'a.*.c[*]')).toEqual(1); - expect(compare('a.*.c[1]', 'a.*.c')).toEqual(1); - expect(compare('a.*.c', 'a.*.c[*]')).toEqual(0); - - expect(compare('[0]', '[0]')).toEqual(0); - expect(compare('[1][0]', '[2][0]')).toEqual(-1); - expect(compare('[1]', '[0]')).toEqual(1); - expect(compare('![1]', '[0]')).toEqual(1); - // higher (last item) index should come first both if negeated. ![*] - // should come last. - expect(compare('![*]', '![0]')).toEqual(1); - expect(compare('![1]', '![0]')).toEqual(-1); - expect(compare('![1][*]', '![1][1]')).toEqual(1); - expect(compare('![*][1]', '![0][2]')).toEqual(1); - expect(compare('![*][1]', '![1][1]')).toEqual(-1); - // not same array items, regular sorting - expect(compare('![1][0]', '![2][0]')).toEqual(-1); - }); - - test('.sort()', () => { - const globs = [ - '!prop.name', - 'bill.account.credit', - 'bill.account.*', - '!*.account.*.name', - 'account.tags', - '!account.id', - 'bill.*.*', - 'prop.id', - 'account.likes[*]', - '*.account.id', - '*', - '!*.account.*', - '*.*.credit', - 'account.*', - 'account.id', - 'prop.x[2].y', - '!x[1].foo', - 'prop.*', - '!prop.*.name', - 'x[*].foo', - '!foo.*.boo', - 'foo.qux.*' - ]; - - const expectedSorted = [ - '*', - 'account.*', - 'bill.*.*', - 'prop.*', - 'account.id', - 'account.likes[*]', - 'account.tags', - 'bill.account.*', - 'foo.qux.*', - 'prop.id', - '!account.id', - '!prop.name', - '*.*.credit', - '*.account.id', - 'x[*].foo', - '!*.account.*', - '!foo.*.boo', - '!prop.*.name', - 'bill.account.credit', - '!x[1].foo', - '!*.account.*.name', - 'prop.x[2].y' - ]; - - // '!foo.*.boo' vs 'foo.qux.*' => '!foo.*.boo', 'foo.qux.*' - // 'foo.*.boo' vs '!foo.qux.*' => 'foo.*.boo', '!foo.qux.*' - - let i, shuffled; - function indexOf(v) { - return shuffled.indexOf(v); - } - for (i = 0; i <= 10; i++) { - shuffled = shuffle(globs.concat()); - shuffled = sort(shuffled); - // console.log(shuffled); - expect(shuffled).toEqual(expectedSorted); - expect(indexOf('*')).toEqual(0); - expect(indexOf('account.*')).toBeLessThan(indexOf('account.tags')); - expect(indexOf('account.*')).toBeLessThan(indexOf('account.id')); - expect(indexOf('account.*')).toBeLessThan(indexOf('!account.id')); - expect(indexOf('account.id')).toBeLessThan(indexOf('!account.id')); - expect(indexOf('bill.*.*')).toBeLessThan(indexOf('bill.account.credit')); - expect(indexOf('bill.account.*')).toBeLessThan(indexOf('bill.account.credit')); - expect(indexOf('bill.*.*')).toBeLessThan(indexOf('bill.account.*')); - expect(indexOf('*.account.*')).toBeLessThan(indexOf('*.account.id')); - expect(indexOf('*.account.id')).toBeLessThan(indexOf('!*.account.*')); - expect(indexOf('*.account.id')).toBeLessThan(indexOf('!*.account.*.name')); - expect(indexOf('*.*.credit')).toBeLessThan(indexOf('!*.account.*.name')); - expect(indexOf('*.*.credit')).toBeLessThan(indexOf('bill.account.credit')); - expect(indexOf('prop.*')).toBeLessThan(indexOf('prop.id')); - expect(indexOf('prop.*')).toBeLessThan(indexOf('!prop.*.name')); - expect(indexOf('x[*].foo')).toBeLessThan(indexOf('!x[1].foo')); - expect(indexOf('prop.*')).toBeLessThan(indexOf('prop.x[2].y')); - } - }); - - test('.sort() » negated comes last', () => { - const original = [ - 'foo.bar.baz', - 'bar.name', - '!foo.*.baz', - '!bar.*', - '!foo.qux.boo', - 'foo.qux.*', - 'bar.id', - '!bar.id' - ]; - - let i, shuffled, sorted, indexN, indexNeg; - for (i = 0; i <= 10; i++) { - shuffled = shuffle(original.concat()); - // console.log(shuffled); - sorted = sort(shuffled); - indexN = sorted.indexOf('bar.id'); - indexNeg = sorted.indexOf('!bar.id'); - - expect(indexNeg).toBeGreaterThan(-1); - expect(indexN).toBeGreaterThan(-1); - expect(indexNeg).toBeGreaterThan(indexN); - } - }); - - test('constructor, .create()', () => { - const g1 = new Notation.Glob('!*.x[1].*'); - expect(g1 instanceof Notation.Glob).toEqual(true); - expect(g1.glob).toEqual('!*.x[1].*'); - expect(g1.absGlob).toEqual('*.x[1].*'); - expect(g1.isNegated).toEqual(true); - expect(g1.regexp.source).toEqual('^' + reVAR + '\\.x\\[1\\]\\.' + reVAR + reREST); - expect(g1.regexp.flags).toEqual('i'); - expect(g1.notes).toEqual(['*', 'x', '[1]', '*']); - expect(g1.levels).toEqual(['*', 'x', '[1]', '*']); // alias - expect(g1.test('y')).toEqual(false); - expect(g1.test('y.x[1]')).toEqual(false); - expect(g1.test('y.x[1].z')).toEqual(true); - expect(g1.test('y.z')).toEqual(false); - - const g2 = create('[*].x'); - expect(g2 instanceof Notation.Glob).toEqual(true); - expect(g2.glob).toEqual('[*].x'); - expect(g2.absGlob).toEqual('[*].x'); - expect(g2.isNegated).toEqual(false); - expect(g2.regexp.source).toEqual('^' + reARRINDEX + '\\.x' + reREST); - expect(g2.regexp.flags).toEqual('i'); - expect(g2.notes).toEqual(['[*]', 'x']); - expect(g2.levels).toEqual(['[*]', 'x']); // alias - expect(g2.test('[0]')).toEqual(false); - expect(g2.test('[1].x')).toEqual(true); - expect(g2.test('[1].y')).toEqual(false); - - expect(() => g2.test('*')).toThrow(); - expect(() => g2.test('[1].*')).toThrow(); - - expect(() => new Notation.Glob()).toThrow(); - expect(() => new Notation.Glob('')).toThrow(); - expect(() => new Notation.Glob('%.s')).toThrow(); - expect(() => create()).toThrow(); - expect(() => create('')).toThrow(); - expect(() => create('s .')).toThrow(); - }); - - test('#parent, #last, #first', () => { - expect(create('*').parent).toEqual(null); - expect(create('*.x').parent).toEqual('*'); - expect(create('*.x.*').parent).toEqual('*'); - expect(create('*.x[*]').parent).toEqual('*'); - expect(create('[*].x').parent).toEqual('[*]'); - expect(create('x.y.z').parent).toEqual('x.y'); - - expect(create('*').last).toEqual('*'); - expect(create('*.x').last).toEqual('x'); - expect(create('*.x.*').last).toEqual('x'); - expect(create('*.x[*]').last).toEqual('x'); - expect(create('[*].x').last).toEqual('x'); - expect(create('x.y.z').last).toEqual('z'); - - expect(create('*').first).toEqual('*'); - expect(create('*.x').first).toEqual('*'); - expect(create('*.x.*').first).toEqual('*'); - expect(create('*.x[*]').first).toEqual('*'); - expect(create('[*].x').first).toEqual('[*]'); - expect(create('x.y.z').first).toEqual('x'); - }); - - test('#test()', () => { - let strNota = 'account.id'; - expect(create('account.id').test(strNota)).toEqual(true); - expect(create('account.*').test(strNota)).toEqual(true); - expect(create('*.*').test(strNota)).toEqual(true); - expect(create('*').test(strNota)).toEqual(true); - expect(create('billing.account.id').test(strNota)).toEqual(false); - expect(create(strNota).test('billing.account.id')).toEqual(false); - - strNota = 'list[1].id'; - expect(create('list[1].id').test(strNota)).toEqual(true); - expect(create('list[2].id').test(strNota)).toEqual(false); - expect(create('list[*]').test(strNota)).toEqual(true); - expect(create('list[*].*').test(strNota)).toEqual(true); - expect(create('*').test(strNota)).toEqual(true); - expect(create('[*]').test(strNota)).toEqual(false); - expect(create('x.list[1].id').test(strNota)).toEqual(false); - expect(create(strNota).test('x.list[2].id')).toEqual(false); - - strNota = '[1].id'; - expect(create('[1].id').test(strNota)).toEqual(true); - expect(create('[2].id').test(strNota)).toEqual(false); - expect(create('[*]').test(strNota)).toEqual(true); - expect(create('[*].*').test(strNota)).toEqual(true); - expect(create('*').test(strNota)).toEqual(false); - expect(create('[*]').test(strNota)).toEqual(true); - expect(create('x[1].id').test(strNota)).toEqual(false); - expect(create(strNota).test('x[1].id')).toEqual(false); - }); - - test('#covers(), ._covers()', () => { - const cov = (globA, globB) => create(globA).covers(globB); - - expect(cov('*.*', 'b')).toEqual(true); - expect(cov('*.b', 'b')).toEqual(false); - expect(cov('a.*', 'b')).toEqual(false); - expect(cov('a.*', 'a')).toEqual(true); - expect(cov('*', 'b')).toEqual(true); - expect(cov('a', 'b')).toEqual(false); - expect(cov('a.b.c', 'a.b.c')).toEqual(true); - expect(cov('a.b', 'a.b.c')).toEqual(true); - expect(cov('a.b.c', 'a.b')).toEqual(false); - expect(cov('*', 'b.*')).toEqual(true); - expect(cov('*', 'b.*')).toEqual(true); - expect(cov('b.*', '*')).toEqual(false); - expect(cov('a', '*')).toEqual(false); - expect(cov('a', 'b.*')).toEqual(false); - expect(cov('a', 'a')).toEqual(true); - expect(cov('a', '!a')).toEqual(true); - expect(cov('!a', 'a')).toEqual(true); - expect(cov('a.*', 'a.b')).toEqual(true); - expect(cov('a.*', 'a.b[1]')).toEqual(true); - expect(cov('a.*', 'a.b[*].c')).toEqual(true); - expect(cov('a.b[*].c', 'a.b[*]')).toEqual(false); - expect(cov('a.b[*]', new Notation.Glob('a.b[2].c'))).toEqual(true); - expect(cov('[1].*.b[*].*.d', '[1].a.b[3].c.d')).toEqual(true); - expect(cov('[1].*.b[*].*.d', '[2].a.b')).toEqual(false); - - // static private method: _covers() - expect(_covers('*', 'b')).toEqual(true); - expect(_covers('a', 'b')).toEqual(false); - expect(_covers('a.b.c', 'a.b.c')).toEqual(true); - expect(_covers('*', '*.b')).toEqual(true); - expect(_covers('a.b[*].c', 'a.b[*]')).toEqual(false); - expect(_covers('a.b[*]', new Notation.Glob('a.b[2].c'))).toEqual(true); - expect(_covers('!*', 'b')).toEqual(true); - expect(_covers('b', '!*')).toEqual(false); - expect(_covers('!*.b', 'a.b')).toEqual(true); - expect(_covers('a.b', '!*.b')).toEqual(false); - expect(_covers('!*.b', 'y.b.c')).toEqual(true); - expect(_covers('![*].b', '[2].b.c')).toEqual(true); - expect(_covers('!*.b', '["2"].b.c')).toEqual(true); - - // check for 'match' instead of 'covers' (3rd arg set to `true`) - expect(_covers('[*]', '[0]', true)).toEqual(true); - expect(_covers('[0]', '[*]', true)).toEqual(true); - expect(_covers('[*]', '[1]', true)).toEqual(true); - expect(_covers('[1]', '[*]', true)).toEqual(true); - expect(_covers('[0][*]', '[*][1]', true)).toEqual(true); - expect(_covers('[*][1]', '[0][*]', true)).toEqual(true); - expect(_covers('[*][*]', '[*][*]', true)).toEqual(true); - expect(_covers('[4][*]', '[*][*][1]', true)).toEqual(true); - - // object key bracket notation - expect(_covers("*['x']", 'a.x')).toEqual(true); - expect(_covers('*["x"]', 'a.x')).toEqual(true); - expect(_covers('*["x"]', 'a["x"]')).toEqual(true); - expect(_covers("*['x']", "a['x']")).toEqual(true); - expect(_covers('*', "['x']")).toEqual(true); - expect(_covers('*', '["x.y"]')).toEqual(true); - expect(_covers('[*]', "['x']")).toEqual(false); - expect(_covers('*', "['*']")).toEqual(true); - expect(_covers('*', "['[*]']")).toEqual(true); - expect(_covers('[*]', "['[*]']")).toEqual(false); - expect(_covers('x.y', "['x.y']")).toEqual(false); - expect(_covers('x.y', "x['y']")).toEqual(true); - expect(_covers('x.y', 'x[\'y\']')).toEqual(true); - }); - - test('#intersect(), ._intersect()', () => { - const intersect = (globA, globB, restrictive = false) => create(globA).intersect(globB, restrictive); - // x.* ∩ *.y » x.y - // x.*.* ∩ *.y » x.y.* - // x.*.z ∩ *.y » x.y.z - // x.y ∩ *.b » (n/a) - // x.y ∩ a.* » (n/a) - expect(_intersect('x.*', '*.z')).toEqual('x.z'); - expect(_intersect('x.*', '*.z', true)).toEqual('x.z'); - expect(_intersect('x.*.*', '*.y')).toEqual('x.y'); - expect(_intersect('x.*.z', '!*.y')).toEqual('x.y.z'); // asuming y is object - expect(_intersect('!x.*.z', '*.y')).toEqual('!x.y.z'); // asuming y is object - expect(_intersect('x.*.z', '!*.y', true)).toEqual('!x.y.z'); // asuming y is object - expect(_intersect('x.y', '*.b')).toEqual(null); - expect(_intersect('x.y', 'a.*')).toEqual(null); - expect(_intersect('x.*.*.z.*', 'x.a.*.z.b')).toEqual('x.a.*.z.b'); - expect(_intersect('!x.*.*.z.x', 'x.*.*.z.*')).toEqual('!x.*.*.z.x'); - expect(_intersect('!x.*.*.z.x', 'x.*.*.z.*', true)).toEqual('!x.*.*.z.x'); - expect(_intersect('x.*.*.z.x', 'x.*.*.z.y')).toEqual(null); - expect(_intersect('x.a.*.z.x', 'x.b.*.z.y')).toEqual(null); - expect(_intersect('x.*.*.z.*', 'x.*')).toEqual('x.*.*.z'); - expect(_intersect('x.*.*', 'x.o')).toEqual('x.o'); - expect(_intersect('x.*[*]', 'x.o')).toEqual('x.o'); - expect(_intersect('!x.*.*', '!x.o')).toEqual('!x.o.*'); - expect(_intersect('a.*', '!*.z')).toEqual('!a.z'); - expect(_intersect('*.z', '!a.*')).toEqual('a.z'); - expect(_intersect('*.z', '!a.*', true)).toEqual('!a.z'); - expect(_intersect('*.z', 'a.*')).toEqual('a.z'); - - // instance method #intersect() also normalizes the glob - expect(intersect('x.*.*', '*.y')).toEqual('x.y'); - expect(intersect('x.*.z', '!*.y')).toEqual('x.y.z'); - expect(intersect('x.*.z', '!*.y', true)).toEqual('!x.y.z'); - expect(intersect('x.y', '*.b')).toEqual(null); - expect(intersect('x.*[*]', 'x.o')).toEqual('x.o'); - expect(intersect('!x.*.*', '!x.o')).toEqual('!x.o.*'); - expect(intersect('a.*', '!*.z')).toEqual('!a.z'); - expect(intersect('*.z', '!a.*')).toEqual('a.z'); - expect(intersect('*.z', '!a.*', true)).toEqual('!a.z'); - expect(intersect('*.z', 'a.*')).toEqual('a.z'); - - // check default value for restrictive argument - expect(create('*.z').intersect('!a.*')).toEqual('a.z'); - expect(create('*.z').intersect('!a.*', true)).toEqual('!a.z'); - }); - - test('.normalize() restrictive = false', () => { - const norm = globs => normalize(globs, false); - - expect(() => norm(['*.[*]'])).toThrow(); - expect(() => norm(['*.[*]', 'x'])).toThrow(); - expect(() => norm(['*[*]*[*]'])).toThrow(); - expect(() => norm(['*[*'])).toThrow(); - expect(() => norm(['*', 'x-1'])).toThrow(); - - expect(norm(['*'])).toEqual(['*']); - expect(norm(['*.*'])).toEqual(['*']); - expect(norm(['*.*.*'])).toEqual(['*']); - expect(norm(['*[*].*'])).toEqual(['*']); - expect(norm(['*[*].*[*]'])).toEqual(['*']); - expect(norm(['*', '*'])).toEqual(['*']); - expect(norm(['x', 'x'])).toEqual(['x']); - expect(norm(['[*]'])).toEqual(['[*]']); - expect(norm(['!*'])).toEqual([]); - expect(norm(['![*]'])).toEqual([]); - expect(norm(['*', '!*'])).toEqual([]); - expect(norm(['!*', '*'])).toEqual([]); - - // higher arr index item globs should come first, if negated bec. they - // should be removed first to prevent shifted indexes. - expect(norm(['[*]', '![*][1]', '![0][1]', '![0][2]', '![1][2][0]'])) - .toEqual(['[*]', '![0][2]', '![*][1]', '![1][2][0]']); - expect(norm(['![1][0][3]', '[*]', '![1][*][5]', '![*][0][4]', '![0][3]', '![1][2][3]'])) - .toEqual(['[*]', '![0][3]', '![1][*][5]', '![*][0][4]', '![1][0][3]', '![1][2][3]']); - - expect(norm(['*', 'user', 'video'])).toEqual(['*']); - expect(norm(['!*', 'user', 'video'])).toEqual(['user', 'video']); - expect(norm(['user', 'video', '!*'])).toEqual(['user', 'video']); // order shouldn't matter - expect(norm(['!*', '!user', 'video'])).toEqual(['video']); - - expect(norm(['user.*', '!user.id'])).toEqual(['user', '!user.id']); - expect(norm(['!user.*', 'user.id'])).toEqual(['user.id']); // more explicit wins - expect(norm(['!*.id', 'user.id'])).toEqual(['user.id']); // more explicit wins - - expect(norm(['*', '!*.id', 'user.id'])).toEqual(['*', '!*.id', 'user.id']); // explicit remains - expect(norm(['user', '!*.id', 'user.id'])).toEqual(['user']); // explicit remains - - expect(norm(['!user.id', 'user.id'])).toEqual([]); // exact negated wins - expect(norm(['*', '!user.*', 'user.id'])).toEqual(['*', '!user.*', 'user.id']); - expect(norm(['user', '!*.id'])).toEqual(['user', '!user.id']); - expect(norm(['*', 'user', '!*.id'])).toEqual(['*', '!*.id']); - - expect(norm(['user', '!*.id', 'car.id'])).toEqual(['user', 'car.id', '!user.id']); // explicit remains - expect(norm(['user', '!*.id', 'car.id', 'user.id'])).toEqual(['user', 'car.id']); // explicit remains - - expect(norm(['!user.*.*', 'user.profile'])).toEqual(['user.profile', '!user.profile.*']); - - expect(norm(['user', 'user.id', '!*.id', 'video.id'])).toEqual(['user', 'video.id']); - expect(norm(['*', 'user', 'user.id', '!*.id', 'video.id'])).toEqual(['*', '!*.id', 'user.id', 'video.id']); - expect(norm(['video.id', '!*.id', 'user', 'user.id', '*'])).toEqual(['*', '!*.id', 'user.id', 'video.id']); // re-ordered version of above - expect(norm(['*', 'user', '!*.id', 'video.id'])).toEqual(['*', '!*.id', 'video.id']); - - expect(norm(['*', 'user.id', '!*.id', 'video.id'])).toEqual(['*', '!*.id', 'user.id', 'video.id']); - - expect(norm(['*', '!user.pwd'])).toEqual(['*', '!user.pwd']); - expect(norm(['*', 'user.*', '!user.pwd'])).toEqual(['*', '!user.pwd']); - - expect(norm(['*', '!id', 'name', 'car.model', '!car.*', 'id', 'name', 'user', '!user.pwd'])) - .toEqual(['*', '!id', '!car.*', 'car.model', '!user.pwd']); - - expect(norm(['*', '!id', 'car.model', '!car.*', '!user.pwd'])) - .toEqual(['*', '!id', '!car.*', 'car.model', '!user.pwd']); - - expect(norm(['*', 'car.model', '!car'])).toEqual(['*', '!car', 'car.model']); - - expect(norm(['name', 'pwd', '!id'])).toEqual(['name', 'pwd']); - - expect(norm(['!x.o.y', 'x.o'])).toEqual(['x.o', '!x.o.y']); - expect(norm(['!x.o.*', 'x.o'])).toEqual(['x.o', '!x.o.*']); - expect(norm(['!x.*.*', 'x.o'])).toEqual(['x.o', '!x.o.*']); - expect(norm(['!x.*.*', '*', 'x.o', 'id'])).toEqual(['*', '!x.*.*']); - - expect(norm(['!a.*', 'a'])).toEqual(['a', '!a.*']); // Notation#filter() would return {} - // should be treated same as above - expect(norm(['!a[*]', 'a'])).toEqual(['a', '!a[*]']); // Notation#filter() would return [] - expect(norm(['*', 'a[*]', '!a[*]'])).toEqual(['*', '!a[*]']); - expect(norm(['a[*]', '!a[*]'])).toEqual(['a', '!a[*]']); - expect(norm(['a[*]', '!a'])).toEqual([]); - expect(norm(['!a', 'a[*]'])).toEqual([]); - expect(norm(['!a[*]', 'a[*]'])).toEqual(['a', '!a[*]']); // results in empty array - expect(norm(['!a[*]', 'a[1]'])).toEqual(['a[1]']); - expect(norm(['*', '!a[*]', 'a[1]'])).toEqual(['*', '!a[*]', 'a[1]']); - expect(norm(['a[*]', '!a[0][1][2]'])).toEqual(['a', '!a[0][1][2]']); - expect(norm(['a[4]', 'a[*]'])).toEqual(['a']); - expect(norm(['a.*', 'a.*[*]'])).toEqual(['a']); - expect(norm(['a.*', 'a.*[2]'])).toEqual(['a']); - expect(norm(['a.b', 'a.*[*]', 'a.c[2].*'])).toEqual(['a']); - - expect(norm(['!x', 'c[1]', '!c[*]', '*', '!d.e'])) - .toEqual(['*', '!x', '!c[*]', 'c[1]', '!d.e']); - - expect(norm(['!id', 'name', 'car.model', '!car.*', 'id', '!email'])) - .toEqual(['name', 'car.model']); - - expect(norm(['!y.*', 'x.x[1][0][*]', '*.x[*]', '!x.x[2][*]', 'a.b', 'c[*][1]'])) - .toEqual(['*.x', 'a.b', 'c[*][1]', '!x.x[2][*]']); - - expect(norm(['bar.name', '!bar.id', 'bar', 'foo.bar.baz', 'foo.qux', '!bar.id', 'bar.id', '!foo.*.baz'])) - .toEqual(['bar', 'foo.qux', '!bar.id', 'foo.bar.baz', '!foo.qux.baz']); - - expect(norm(['!*', 'a'])).toEqual(['a']); - expect(norm(['!*.b', 'a.b'])).toEqual(['a.b']); - expect(norm(['!a', 'a.b', 'x[*].*'])).toEqual(['x', 'a.b']); - expect(norm(['a.*', '!*.z'])).toEqual(['a', '!a.z']); - expect(norm(['a', '!a.z'])).toEqual(['a', '!a.z']); - expect(norm(['x.*', '!*.y'])).toEqual(['x', '!x.y']); - - expect(norm(['!a.x', 'a.x', '!a.*', '!*'])).toEqual([]); - - expect(norm(['!a.*', '*.x'])).toEqual(['*.x']); - expect(norm(['!a.*', '*.x', '*.y'])).toEqual(['*.x', '*.y']); - expect(norm(['a', 'b.c', '!x', '*.y'])).toEqual(['a', '*.y', 'b.c']); - expect(norm(['a', 'b.c', '!x', '!*.y'])).toEqual(['a', 'b.c', '!a.y']); - expect(norm(['*', 'a', '!x', '!*.y'])).toEqual(['*', '!x', '!*.y']); - expect(norm(['!a.b.*', 'a.b.c'])).toEqual(['a.b.c']); - expect(norm(['!a.*', '!a.b.*', 'a.b.c'])).toEqual(['a.b.c']); - - expect(norm(['!x.*', 'x.y'])).toEqual(['x.y']); - expect(norm(['x.*', '!*.y'])).toEqual(['x', '!x.y']); - expect(norm(['a', 'x', '!a.z', '!x.y'])).toEqual(['a', 'x', '!a.z', '!x.y']); - expect(norm(['*', '!*.b', 'x.b', 'a'])).toEqual(['*', '!*.b', 'x.b']); - expect(norm(['*.x', '!*.*.b', 'x.a.b'])).toEqual(['*.x', '!*.x.b', 'x.a.b']); - expect(norm(['*', '*.x', '!*.*.b', 'x.a.b'])).toEqual(['*', '!*.*.b', 'x.a.b']); - - expect(norm(['!*.b', 'a.b', 'x.b', 'y.b', 'z.b'])).toEqual(['a.b', 'x.b', 'y.b', 'z.b']); - expect(norm(['!x', 'x.b', 'x.b.c', '!*.b.c'])).toEqual(['x.b']); - expect(norm(['!x', 'x.y', 'x.y.z', 'o.y.z', '!*.y.z', '!a', 'a.b', 'q.b.c', '!*.b.c'])) - .toEqual(['a.b', 'x.y', 'o.y.z', 'q.b.c', '!a.b.c']); - expect(norm(['!x', 'x.b', 'x.b.c', '!*.b.c', '!*.*.c', '!f', '!d'])).toEqual(['x.b']); - - expect(norm(['!x', '!x.b', '!x.b.c', '*.b'])).toEqual(['*.b']); - - expect(norm(['!x', '!x.b', '!x.b.c', '*.b', '!*.b'])).toEqual([]); - expect(norm(['!*.b', 'x.b', 'x.b.c', 'x'])).toEqual(['x']); - expect(norm(['!*.b', 'x.b', 'x.b.c', 'x', 'y.b.c'])).toEqual(['x', 'y.b.c']); - - expect(norm(['*.b', '!a.b', '!x.b', '!y.b', '!z.b'])).toEqual(['*.b', '!a.b', '!x.b', '!y.b', '!z.b']); - expect(norm(['*.b', 'x', '!*.b', 'y'])).toEqual(['x', 'y', '!x.b', '!y.b']); - - // cannot have both object and array notations for root level - expect(() => norm(['a.b', '[1].b'])).toThrow(); - }); - - test('.normalize() restrictive = true', () => { - const norm = globs => normalize(globs, true); - - expect(() => norm(['*.[*]'])).toThrow(); - expect(() => norm(['*.[*]', 'x'])).toThrow(); - expect(() => norm(['*[*]*[*]'])).toThrow(); - expect(() => norm(['*[*'])).toThrow(); - expect(() => norm(['*', 'x-1'])).toThrow(); - - expect(norm(['*'])).toEqual(['*']); - expect(norm(['*.*'])).toEqual(['*']); - expect(norm(['*.*.*'])).toEqual(['*']); - expect(norm(['*[*].*'])).toEqual(['*']); - expect(norm(['*[*].*[*]'])).toEqual(['*']); - expect(norm(['*', '*'])).toEqual(['*']); - expect(norm(['x', 'x'])).toEqual(['x']); - expect(norm(['[*]'])).toEqual(['[*]']); - expect(norm(['!*'])).toEqual([]); - expect(norm(['![*]'])).toEqual([]); - expect(norm(['*', '!*'])).toEqual([]); - expect(norm(['!*', '*'])).toEqual([]); - - expect(norm(['*', 'user', 'video'])).toEqual(['*']); - expect(norm(['!*', 'user', 'video'])).toEqual([]); - expect(norm(['user', 'video', '!*'])).toEqual([]); // order shouldn't matter - expect(norm(['!*', '!user', 'video'])).toEqual([]); - - expect(norm(['*', 'name', 'pwd', 'id'])).toEqual(['*']); - expect(norm(['name', 'pwd', 'id'])).toEqual(['id', 'name', 'pwd']); - expect(norm(['*', 'name', 'pwd', '!id'])).toEqual(['*', '!id']); - expect(norm(['user.*', '!user.pwd'])).toEqual(['user', '!user.pwd']); - expect(norm(['*', '!*.id'])).toEqual(['*', '!*.id']); - - expect(norm(['!*.id', 'x.id'])).toEqual([]); - expect(norm(['user', '!*.id', 'x.id'])).toEqual(['user', '!user.id']); - - expect(norm(['*', '!user.pwd'])).toEqual(['*', '!user.pwd']); - expect(norm(['*', 'user.*', '!user.pwd'])).toEqual(['*', '!user.pwd']); - - expect(norm(['*', '!id', 'name', 'car.model', '!car.*', 'id', 'name', 'user', '!user.pwd'])) - .toEqual(['*', '!id', '!car.*', '!user.pwd']); - - expect(norm(['*', '!id', 'car.model', '!car.*', '!user.pwd'])) - .toEqual(['*', '!id', '!car.*', '!user.pwd']); - - expect(norm(['name', 'pwd', '!id'])).toEqual(['name', 'pwd']); - - expect(norm(['!x.*.*', '*', 'x.o', 'id'])).toEqual(['*', '!x.*.*']); - - expect(norm(['!a.*', 'a'])).toEqual(['a', '!a.*']); // Notation#filter() would return {} - // should be treated same as above - expect(norm(['!a[*]', 'a'])).toEqual(['a', '!a[*]']); // Notation#filter() would return [] - expect(norm(['*', 'a[*]', '!a[*]'])).toEqual(['*', '!a[*]']); - expect(norm(['a[*]', '!a[*]'])).toEqual(['a', '!a[*]']); - expect(norm(['a[*]', '!a'])).toEqual([]); - expect(norm(['!a', 'a[*]'])).toEqual([]); - expect(norm(['a[*]', '!a[0][1][2]'])).toEqual(['a', '!a[0][1][2]']); - expect(norm(['a[4]', 'a[*]'])).toEqual(['a']); - expect(norm(['a.*', 'a.*[*]'])).toEqual(['a']); - expect(norm(['a.*', 'a.*[2]'])).toEqual(['a']); - - expect(norm(['a.b', 'a.*[*]', 'a.c[2].*'])).toEqual(['a']); - - expect(norm(['!x', 'c[1]', '!c[*]', '*', '!d.e'])) - .toEqual(['*', '!x', '!c[*]', '!d.e']); - - expect(norm(['!id', 'name', 'car.model', '!car.*', 'id', '!email'])) - .toEqual(['name']); - - expect(norm(['!y.*', 'x.x[1][0][*]', '*.x[*]', '!x.x[2][*]', 'a.b', 'c[*][1]'])) - .toEqual(['*.x', 'a.b', '!y.x', 'c[*][1]', '!x.x[2][*]']); - - expect(norm(['bar.name', '!bar.id', 'bar', 'foo.bar.baz', 'foo.qux', '!bar.id', 'bar.id', '!foo.*.baz'])) - .toEqual(['bar', 'foo.qux', '!bar.id', '!foo.qux.baz']); - - expect(norm(['!*', 'a'])).toEqual([]); - expect(norm(['!*.b', 'a.b'])).toEqual([]); - expect(norm(['!a', 'a.b', 'x[*].*'])).toEqual(['x']); - expect(norm(['a.*', '!*.z'])).toEqual(['a', '!a.z']); - expect(norm(['x.*', '!*.y'])).toEqual(['x', '!x.y']); - expect(norm(['!a.x', 'a.x', '!a.*', '!*'])).toEqual([]); - expect(norm(['!a.*', '*.x'])).toEqual(['*.x', '!a.x']); - expect(norm(['!a.*', '*.x', '*.y'])).toEqual(['*.x', '*.y', '!a.x', '!a.y']); - expect(norm(['a', 'b.c', '!x', '*.y'])).toEqual(['a', '*.y', 'b.c', '!x.y']); - expect(norm(['a', 'b.c', '!x', '!*.y'])).toEqual(['a', 'b.c', '!a.y']); - expect(norm(['*', 'a', '!x', '!*.y'])).toEqual(['*', '!x', '!*.y']); - expect(norm(['!a.b.*', 'a.b.c'])).toEqual([]); - expect(norm(['!a.*', '!a.b.*', 'a.b.c'])).toEqual([]); - - expect(norm(['!x.*', 'x.y'])).toEqual([]); - expect(norm(['x.*', '!*.y'])).toEqual(['x', '!x.y']); - expect(norm(['a', 'x', '!a.z', '!x.y'])).toEqual(['a', 'x', '!a.z', '!x.y']); - expect(norm(['*', '!*.b', 'x.b', 'a'])).toEqual(['*', '!*.b']); - expect(norm(['*.x', '!*.*.b', 'x.a.b'])).toEqual(['*.x', '!*.x.b']); - expect(norm(['*', '*.x', '!*.*.b', 'x.a.b'])).toEqual(['*', '!*.*.b']); - - expect(norm(['!*.b', 'a.b', 'x.b', 'y.b', 'z.b'])).toEqual([]); - expect(norm(['!x', 'x.b', 'x.b.c', '!*.b.c'])).toEqual([]); - expect(norm(['!x', 'x.y', 'x.y.z', 'o.y.z', '!*.y.z', '!a', 'a.b', 'q.b.c', '!*.b.c'])).toEqual([]); - expect(norm(['!x', 'x.b', 'x.b.c', '!*.b.c', '!*.*.c', '!f', '!d'])).toEqual([]); - - expect(norm(['!x', '!x.b', '!x.b.c', '*.b'])).toEqual(['*.b', '!x.b']); - - expect(norm(['!x', '!x.b', '!x.b.c', '*.b', '!*.b'])).toEqual([]); - expect(norm(['!*.b', 'x.b', 'x.b.c', 'x'])).toEqual(['x', '!x.b']); - expect(norm(['!*.b', 'x.b', 'x.b.c', 'x', 'y.b.c'])).toEqual(['x', '!x.b']); - - expect(norm(['*.b', '!a.b', '!x.b', '!y.b', '!z.b'])).toEqual(['*.b', '!a.b', '!x.b', '!y.b', '!z.b']); - expect(norm(['*.b', 'x', '!*.b', 'y'])).toEqual(['x', 'y', '!x.b', '!y.b']); - }); - - test('.normalize() » issue #7', () => { - const globs = [ - '!pass', - '!password.prop', - '*', - '!password', - '!password_reset_code' - ]; - // console.log(normalize(globs)); - expect(normalize(globs)).toEqual([ - '*', - '!pass', - '!password', - '!password_reset_code' - ]); - }); - - test('.union() restrictive = false', () => { - const uni = (a, b) => union(a, b, false); - - expect(uni([], ['*', 'x.y'])).toEqual(['*']); - expect(uni(['*', 'x.y'], [])).toEqual(['*']); - expect(uni([], ['!x.*', 'x.y'])).toEqual(['x.y']); - expect(uni(['!x.*', 'x.y'], [])).toEqual(['x.y']); - expect(uni(['a.b', '*.z'], ['!x.*', 'x.y'])).toEqual(['*.z', 'a.b', 'x.y']); - - expect(uni( - ['*', 'a', 'b', '!id', '!x.*'], - ['*', '!b', 'id', '!pwd', 'x.o'] - )).toEqual(['*']); - - expect(uni( - ['*', '!id', '!x.*'], - ['*', 'id', '!pwd', '!x.*', 'x.o'] - )).toEqual(['*', '!x.*', 'x.o']); - - expect(uni(['*', '!x.*'], ['!x.*.*'])).toEqual(['*', '!x.*']); - expect(uni(['*', '!x.*'], ['*', '!x.*.*'])).toEqual(['*', '!x.*.*']); - expect(uni(['*', '!x.y'], ['!x.y.z'])).toEqual(['*', '!x.y']); - expect(uni(['*', '!x.y'], ['*', '!x.y.z'])).toEqual(['*', '!x.y.z']); - - expect(uni(['*', '!x.*'], ['*', '!x.*.*'])).toEqual(['*', '!x.*.*']); - - expect(uni( - ['*', '!id', '!x.*'], - ['*', 'id', '!pwd', '!x.*.*', 'x.o'] - )).toEqual(['*', '!x.*.*']); - - expect(uni(['*', '!x[*]'], ['*', '!x[4]', 'x[1]'])).toEqual(['*', '!x[4]']); - - expect(uni( - ['*', 'a', 'b[2]', '!id', '!x[*]'], - ['*', '!b[*]', 'id', '!x[4]', 'x[1]'] - )).toEqual(['*', '!x[4]']); - - expect(uni( - ['*[*]', '!a[1]', '!x[*]'], - ['*', 'a[1]', '!b[*]', '!x[*]', 'x[0]'] - )).toEqual(['*', '!x[*]', 'x[0]']); - - expect(uni( - ['*', '!a[*]', '!x[*]'], - ['*', 'a[*]', '!b', '!x[*].*', 'x[1]'] - )).toEqual(['*', '!x[*].*']); - - expect(uni( - ['*', 'a', 'b[2]', '!id', '!x[*]'], - ['*', '!b[*]', 'id', '!x[4]', 'x[1]'] - )).toEqual(['*', '!x[4]']); - - expect(uni( - ['*', '!a[*]', '!x[*]'], - ['*', 'a[*]', '!b', '!x[*].*', 'x[1]'] - )).toEqual(['*', '!x[*].*']); - - expect(uni(['a.*', '!*.z'], ['x.*', '!*.y'])).toEqual(['a', 'x', '!a.z', '!x.y']); - expect(uni(['*', 'a.*', '!*.z'], ['x.*', '!*.y'])).toEqual(['*', '!*.z']); - expect(uni(['*', 'a.*', '!*.z'], ['*', 'x.*', '!*.y'])).toEqual(['*']); - - // cannot union an obj-glob with an arr-glob - expect(() => union(['*'], ['[*]'])).toThrow(); - }); - - test('.union() restrictive = true', () => { - const uni = (a, b) => union(a, b, true); - - expect(uni([], ['*', 'x.y'])).toEqual(['*']); - expect(uni(['*', 'x.y'], [])).toEqual(['*']); - expect(uni([], ['!x.*', 'x.y'])).toEqual([]); - expect(uni(['!x.*', 'x.y'], [])).toEqual([]); - expect(uni(['a.b', '*.z'], ['!x.*', 'x.y'])).toEqual(['*.z', 'a.b']); - - expect(uni( - ['*', 'a', 'b', '!id', '!x.*'], - ['*', '!b', 'id', '!pwd', 'x.o'] - )).toEqual(['*']); - - expect(uni( - ['*', '!id', '!x.*'], - ['*', 'id', '!pwd', '!x.*', 'x.o'] - )).toEqual(['*', '!x.*']); - - expect(uni(['*', '!x.*'], ['!x.*.*'])).toEqual(['*', '!x.*']); - - expect(uni(['*', '!x.*'], ['*', '!x.*.*'])).toEqual(['*', '!x.*.*']); - - expect(uni( - ['*', '!id', '!x.*'], - ['*', 'id', '!pwd', '!x.*.*', 'x.o'] - )).toEqual(['*', '!x.*.*']); - - expect(uni(['*', '!x[*]'], ['*', '!x[4]', 'x[1]'])).toEqual(['*', '!x[4]']); - - expect(uni( - ['*', 'a', 'b[2]', '!id', '!x[*]'], - ['*', '!b[*]', 'id', '!x[4]', 'x[1]'] - )).toEqual(['*', '!x[4]']); - - expect(uni( - ['*[*]', '!a[1]', '!x[*]'], - ['*', 'a[1]', '!b[*]', '!x[*]', 'x[0]'] - )).toEqual(['*', '!x[*]']); - - expect(uni( - ['*', '!a[*]', '!x[*]'], - ['*', 'a[*]', '!b', '!x[*].*', 'x[1]'] - )).toEqual(['*', '!x[*].*']); - - expect(uni( - ['*', 'a', 'b[2]', '!id', '!x[*]'], - ['*', '!b[*]', 'id', '!x[4]', 'x[1]'] - )).toEqual(['*', '!x[4]']); - - expect(uni( - ['*[*]', '!a[1]', '!x[*]'], - ['*', 'a[1]', '!b[*]', '!x[*]', 'x[0]'] - )).toEqual(['*', '!x[*]']); - - expect(uni( - ['*', '!a[*]', '!x[*]'], - ['*', 'a[*]', '!b', '!x[*].*', 'x[1]'] - )).toEqual(['*', '!x[*].*']); - - expect(uni(['a.*', '!*.z'], ['x.*', '!*.y'])).toEqual(['a', 'x', '!a.z', '!x.y']); - }); - - test('.union() check mutation, order', () => { - const globA = ['foo.bar.baz', 'bar.*', '!bar.id', 'bar.name', '!foo.qux.boo']; - const globB = ['!foo.*.baz', 'bar.id', 'bar.name', '!bar.*', 'foo.qux.*']; - // normalized to: - // globA = [ 'bar', '!bar.id', 'foo.bar.baz' ] - // globB = [ 'foo.qux', '!foo.qux.baz' ] - - // for checking mutation - const cloneGlobA = globA.concat(); - const cloneGlobB = globB.concat(); - - expect(union(globA, globB, true)) - .toEqual(['bar', 'foo.qux', '!bar.id', 'foo.bar.baz', '!foo.qux.baz']); - - // should not mutate given globs arrays - expect(globA).toEqual(cloneGlobA); - expect(globB).toEqual(cloneGlobB); - - // order of parameters should not matter - expect(union(['*'], ['!id'])).toEqual(['*']); - expect(union(['!id'], ['*'])).toEqual(['*']); - expect(union(['id'], ['*'])).toEqual(['*']); - expect(union(['*', '!id'], ['*'])).toEqual(['*']); - expect(union(['*'], ['*', '!id'])).toEqual(['*']); - expect(union(['*'], ['!a[*]'])).toEqual(['*']); - expect(union(['!a[*]'], ['*'])).toEqual(['*']); - expect(union(['a[1]'], ['*'])).toEqual(['*']); - expect(union(['*'], ['a[1]'])).toEqual(['*']); - expect(union(['a[*]', '!a[0]'], ['a[0][*]'])).toEqual(['a']); - expect(union(['a[0][*]'], ['a[*]', '!a[0]'])).toEqual(['a']); - - const a = ['*', '!id']; - const b = ['*', '!pwd']; - const c = ['email']; - const d = ['*']; - // const e = ['*', '!id', '!pwd']; - - expect(union(b, c)).toEqual(['*', '!pwd']); - expect(union(c, d)).toEqual(['*']); - expect(union(a, d)).toEqual(['*']); - expect(union(c, d)).toEqual(['*']); - - expect(union( - ['*', 'a[*]', '!b[2]', '!x[*]', 'o'], - ['*', 'b[2]', '!pwd', 'x[5]', 'o'] - )).toEqual(['*']); - - expect(union( - ['*', 'email', '!id', '!x.*', 'o'], - ['*', 'id', '!pwd', 'x.name', 'o'] - )).toEqual(['*']); - - expect(union( - ['user.*', '!user.email', 'car.model', '!*.id'], - ['!*.date', 'user.email', 'car', '*.age'] - )).toEqual(['car', 'user', '*.age', '!car.date', '!user.id']); - }); - - test('.union() (by intersection)', () => { - let u; - u = union(['*', '!*.z'], ['*', '!x.*']); - expect(u).toEqual(['*', '!x.z']); - u = union(['*', '!*[2]'], ['*', '!x[*]']); - expect(u).toEqual(['*', '!x[2]']); - - // intersection in same (normalize) - expect(union(['a.*', '!*.z'], ['x.*', '!*.y'])).toEqual(['a', 'x', '!a.z', '!x.y']); - expect(union(['a', '!*.z'], ['x', '!*.y'])).toEqual(['a', 'x', '!a.z', '!x.y']); - expect(union(['a.b.*', '!*.*.z'], ['x.*', '!*.y'])).toEqual(['x', 'a.b', '!x.y', '!a.b.z']); - expect(union(['a.b.*', '!*.*.z'], ['x', 'a.b.z', '!*.y'])).toEqual(['x', 'a.b', '!x.y']); - }); - -}); diff --git a/test/notation.spec.js b/test/notation.spec.js deleted file mode 100644 index fe24a27..0000000 --- a/test/notation.spec.js +++ /dev/null @@ -1,685 +0,0 @@ -/* eslint camelcase:0, max-lines-per-function:0, consistent-return:0, max-statements:0, max-lines:0 */ - -import * as _ from 'lodash'; - -import { Notation } from '../src/core/notation'; -import { NotationError } from '../src/core/notation.error'; - -const o = { - name: 'onur', - age: 36, - account: { - id: 15, - tags: 20, - likes: ['movies', '3d', 'programming', 'music'] - }, - billing: { - account: { - id: 121, - credit: 300, - balance: -293 - } - }, - company: { - name: 'pilot co', - address: { - city: 'istanbul', - country: 'TR', - location: { - lat: 34.123123, - lon: 30.123123 - } - }, - account: { - id: 33, - taxNo: 12345 - }, - limited: true, - notDefined: undefined, - nuller: null, - zero: 0 - } -}; - -describe('Notation', () => { - - const { - create, first, last, parent, countNotes, countLevels, - isValid, split, join, eachNote, eachLevel - } = Notation; - - // beforeEach(() => { }); - - test('NotationError', () => { - const errorMessage = 'TEST_ERROR'; - try { - throw new Notation.Error(errorMessage); - } catch (error) { - // console.log(error); - expect(error.name).toEqual('NotationError'); - expect(error.message).toEqual(errorMessage); - expect(error instanceof Error).toEqual(true); - expect(error instanceof Notation.Error).toEqual(true); - expect(error instanceof NotationError).toEqual(true); - expect(Object.prototype.toString.call(error)).toEqual('[object Error]'); - expect(Object.getPrototypeOf(error)).toEqual(NotationError.prototype); - } - expect(new NotationError() instanceof Notation.Error).toEqual(true); - }); - - test('.first(), .last(), .parent()', () => { - let notation = 'first.mid.last'; - expect(first(notation)).toEqual('first'); - expect(last(notation)).toEqual('last'); - expect(parent(notation)).toEqual('first.mid'); - - notation = 'single'; - expect(first(notation)).toEqual('single'); - expect(last(notation)).toEqual('single'); - expect(parent(notation)).toEqual(null); - }); - - test('.countNotes()', () => { - expect(countNotes('a')).toEqual(1); - expect(countNotes('a.b')).toEqual(2); - expect(countLevels('a.b.c')).toEqual(3); // alias - expect(() => countNotes('')).toThrow(); // eslint-disable-line - }); - - test('.isValid()', () => { - expect(isValid('first.mid.last')).toEqual(true); - expect(isValid('first.mid.')).toEqual(false); - expect(isValid('first.')).toEqual(false); - expect(isValid('first')).toEqual(true); - expect(isValid('.first')).toEqual(false); - expect(isValid('.')).toEqual(false); - expect(isValid(null)).toEqual(false); - expect(isValid(true)).toEqual(false); - // star is NOT treated as wildcard here. this is normal dot-notation, - // not a glob. - expect(isValid('prop.*')).toEqual(false); - expect(isValid('prop["*"]')).toEqual(true); - }); - - test('.split(), .join()', () => { - expect(split('a')).toEqual(['a']); - expect(split('[3]')).toEqual(['[3]']); - expect(split('[10].x.y[1].a')).toEqual(['[10]', 'x', 'y', '[1]', 'a']); - expect(split('a.b[0].x.y["5"].z')).toEqual(['a', 'b', '[0]', 'x', 'y', '["5"]', 'z']); - expect(split('a.b[0][1][0]')).toEqual(['a', 'b', '[0]', '[1]', '[0]']); - expect(() => split('')).toThrow(); - expect(() => split('.')).toThrow(); - expect(() => split(' . ')).toThrow(); - expect(() => split('[]')).toThrow(); - expect(() => split('.b')).toThrow(); - expect(() => split('a-b')).toThrow(); - expect(() => split('["a-b"]')).not.toThrow(); - expect(() => split(1)).toThrow(); - expect(() => split({})).toThrow(); - expect(() => split([])).toThrow(); - expect(() => split(null)).toThrow(); - expect(() => split(undefined)).toThrow(); - - expect(join(['[10]', 'x', 'y', '[1]', 'a'])).toEqual('[10].x.y[1].a'); - expect(join(['a', 'b', '[0]', 'x', 'y', '["5"]', 'z'])).toEqual('a.b[0].x.y["5"].z'); - expect(join(['a', 'b', '[0]', '[1]', '[0]'])).toEqual('a.b[0][1][0]'); - }); - - test('.eachNote(), alias: .eachLevel()', () => { - const pattern = 'arr[0].x[1][0].y[5]'; - const result = { - 0: { levelNota: 'arr', note: 'arr' }, - 1: { levelNota: 'arr[0]', note: '[0]' }, - 2: { levelNota: 'arr[0].x', note: 'x' }, - 3: { levelNota: 'arr[0].x[1]', note: '[1]' }, - 4: { levelNota: 'arr[0].x[1][0]', note: '[0]' }, - 5: { levelNota: 'arr[0].x[1][0].y', note: 'y' }, - 6: { levelNota: 'arr[0].x[1][0].y[5]', note: '[5]' } - }; - let c = 0; - eachNote(pattern, (levelNota, note, index) => { - const expected = result[index]; - expect(levelNota).toEqual(expected.levelNota); - expect(note).toEqual(expected.note); - c++; - }); - expect(c).toEqual(countNotes(pattern)); - - // alias test - c = 0; - eachLevel(pattern, (levelNota, note, index) => { - const expected = result[index]; - expect(levelNota).toEqual(expected.levelNota); - expect(note).toEqual(expected.note); - c++; - }); - expect(c).toEqual(countLevels(pattern)); - }); - - test('constructor(), .create()', () => { - expect((new Notation()).set('x', 'value').value.x).toEqual('value'); - expect((new Notation({})).set('x', 'value').value.x).toEqual('value'); - expect((new Notation([])).set('[0]', 'value').value[0]).toEqual('value'); - - expect(() => new Notation(undefined)).toThrow(); - expect(() => new Notation(null)).toThrow(); - expect(() => new Notation(1)).toThrow(); - expect(() => new Notation('test')).toThrow(); - - expect(create().set('x', 'value').value.x).toEqual('value'); - expect(create({}).set('x', 'value').value.x).toEqual('value'); - expect(create([]).set('[0]', 'value').value[0]).toEqual('value'); - - let nota = create(); - expect(nota.options.strict).toEqual(false); - expect(nota.options.preserveIndices).toEqual(false); - nota = create([], { strict: true, preserveIndices: true }); - expect(nota.options.strict).toEqual(true); - expect(nota.options.preserveIndices).toEqual(true); - - expect(() => create(undefined)).toThrow(); - expect(() => create(null)).toThrow(); - expect(() => create(1)).toThrow(); - expect(() => create('test')).toThrow(); - }); - - test('#flatten(), #exapand()', () => { - const nota = new Notation(_.cloneDeep(o)); - const flat = nota.flatten().value; - - // console.log(flat); - expect(flat.name).toEqual(o.name); - expect(flat['account.id']).toEqual(o.account.id); - expect(flat['account.likes']).toBeUndefined(); - expect(flat['account.likes[0]']).toEqual(o.account.likes[0]); - expect(flat['account.likes[1]']).toEqual(o.account.likes[1]); - expect(flat['account.likes[2]']).toEqual(o.account.likes[2]); - expect(flat['account.likes[3]']).toEqual(o.account.likes[3]); - expect(flat['company.name']).toEqual(o.company.name); - expect(flat['company.address.location.lat']).toEqual(o.company.address.location.lat); - - const expanded = create(flat).expand().value; - expect(expanded.name).toEqual(o.name); - expect(expanded.account.id).toEqual(o.account.id); - expect(expanded.account.likes).toEqual(expect.any(Array)); - expect(expanded.company.name).toEqual(o.company.name); - expect(expanded.company.address.location.lat).toEqual(o.company.address.location.lat); - - // alias of expand - const aggregated = create(flat).aggregate().value; - expect(aggregated.name).toEqual(o.name); - expect(aggregated.account.id).toEqual(o.account.id); - expect(aggregated.account.likes.length).toEqual(4); - }); - - test('#each(), #eachValue()', () => { - const assets = { - boat: 'none', - car: { - brand: 'Ford', - model: 'Mustang', - year: 1970, - color: 'red', - other: { - model: null, - colors: ['blue', 'white'], - years: [1965, 1967] - }, - alternate: { - cars: [ - { brand: 'Dodge', model: 'Charger', year: 1970 } - ] - } - } - }; - let nota = create(assets); - let result = []; - nota.each(notation => { - result.push(notation); - }); - const expected = [ - 'boat', - 'car.brand', - 'car.model', - 'car.year', - 'car.color', - 'car.other.model', - 'car.other.colors[0]', - 'car.other.colors[1]', - 'car.other.years[0]', - 'car.other.years[1]', - 'car.alternate.cars[0].brand', - 'car.alternate.cars[0].model', - 'car.alternate.cars[0].year' - ]; - expect(result).toEqual(expected); - - result = []; - nota.each(notation => { - result.push(notation); - return result.length < 4; // should break out on 4 - }); - expect(result.length).toEqual(4); - - let c = 0; - nota.eachValue('car.alternate.cars[0]', (levelValue, levelNotation, note, index) => { - c++; - if (index === 0) expect(levelValue.model).toEqual('Mustang'); - if (index === 1) return false; - }); - expect(c).toEqual(2); - - function A() {} // eslint-disable-line - A.prototype.x = 1; - nota = new Notation(new A()); - c = 0; - nota.eachValue('x', (levelValue, levelNotation, note, index) => { - c++; - if (index === 0) expect(levelValue).toBeUndefined(); - }); - expect(c).toEqual(1); - }); - - test('#getNotations()', () => { - const obj = { a: { b: { c: [{ x: 1 }], d: 2 }, e: 3, f: 4 }, g: [4, [5]] }; - const notations = create(obj).getNotations(); - expect(notations).toEqual(['a.b.c[0].x', 'a.b.d', 'a.e', 'a.f', 'g[0]', 'g[1][0]']); - }); - - test('#inspectGet(), #inspectRemove()', () => { - const obj = { a: { b: [{ c: 1 }] }, d: undefined, e: null, f: [1, false, 2] }; - let nota = create(obj).clone(); - - let ins = nota.inspectGet('a.b[0]'); - expect(ins.notation).toEqual('a.b[0]'); - expect(ins.has).toEqual(true); - expect(ins.value).toEqual({ c: 1 }); - expect(ins.lastNote).toEqual('[0]'); - expect(ins.lastNoteNormalized).toEqual(0); - - ins = nota.inspectGet('d'); - expect(ins.notation).toEqual('d'); - expect(ins.has).toEqual(true); - expect(ins.value).toEqual(undefined); - expect(ins.lastNote).toEqual('d'); - expect(ins.lastNoteNormalized).toEqual('d'); - - ins = nota.inspectRemove('e'); - expect(ins.notation).toEqual('e'); - expect(ins.has).toEqual(true); - expect(ins.value).toEqual(null); - expect(ins.lastNote).toEqual('e'); - expect(ins.lastNoteNormalized).toEqual('e'); - // original should not be mutated - expect(obj.e).toEqual(null); - - ins = nota.inspectRemove('f[1]'); - expect(ins.notation).toEqual('f[1]'); - expect(ins.has).toEqual(true); - expect(ins.value).toEqual(false); - expect(ins.lastNote).toEqual('[1]'); - expect(ins.lastNoteNormalized).toEqual(1); - expect(nota.value.f[1]).toEqual(2); - expect(nota.value.f.length).toEqual(2); - // original should not be mutated - expect(obj.f[1]).toEqual(false); - expect(obj.f.length).toEqual(3); - - let arr = ['a', 'b', 'c']; - nota = create(arr, { preserveIndices: false }); - expect(nota.options.preserveIndices).toEqual(false); - ins = nota.inspectRemove('[1]'); - expect(ins.value).toEqual('b'); - expect(arr[1]).toEqual('c'); - expect(arr.length).toEqual(2); - - arr = ['a', 'b', 'c']; - nota = create(arr, { preserveIndices: true }); - expect(nota.options.preserveIndices).toEqual(true); - ins = nota.inspectRemove('[1]'); - expect(ins.value).toEqual('b'); - expect(arr[1]).toEqual(undefined); - expect(arr.length).toEqual(3); - }); - - test('#has(), #hasDefined()', () => { - const nota = new Notation(_.cloneDeep(o)); - expect(nota.has('name')).toEqual(true); - expect(nota.has('company.address.location.lat')).toEqual(true); - expect(nota.has('company.notDefined')).toEqual(true); - expect(nota.has('notProp1')).toEqual(false); - expect(nota.has('company.notProp2')).toEqual(false); - - expect(nota.hasDefined('name')).toEqual(true); - expect(nota.hasDefined('account.id')).toEqual(true); - expect(nota.hasDefined('company.address.location.lat')).toEqual(true); - expect(nota.hasDefined('company.notDefined')).toEqual(false); - expect(nota.hasDefined('company.none')).toEqual(false); - expect(nota.hasDefined('company.nuller')).toEqual(true); - expect(nota.hasDefined('company.zero')).toEqual(true); - expect(nota.hasDefined('notProp1')).toEqual(false); - expect(nota.hasDefined('company.notProp2')).toEqual(false); - }); - - test('#get()', () => { - let nota = new Notation(_.cloneDeep(o)); - expect(nota.get('name')).toEqual(o.name); - expect(nota.get('account.id')).toEqual(o.account.id); - expect(nota.get('company.address.location.lat')).toEqual(o.company.address.location.lat); - expect(nota.get('account.noProp')).toBeUndefined(); - - nota = new Notation({ a: { b: { c: [null, { d: { e: ['value'] } }, true] } } }); - expect(nota.get('a.b.c')).toEqual(expect.any(Array)); - expect(nota.get('a.b.c[1].d.e')).toEqual(['value']); - expect(nota.get('a.b.c[2]')).toEqual(true); - - const data = { - obj: { a: { b: true } }, - arr: [ - { x: { y: 8 }, z: true }, - { x: { y: 4 }, z: true }, - { x: { y: 6 }, z: true } - ] - }; - nota = new Notation(data); - expect(nota.get('arr[0].x')).toEqual({ y: 8 }); - expect(nota.get('arr[0].x.y')).toEqual(8); - expect(nota.get('arr[1].x')).toEqual({ y: 4 }); - expect(nota.get('arr[1].x.y')).toEqual(4); - expect(nota.get('arr[2].x')).toEqual({ y: 6 }); - expect(nota.get('arr[2].x.y')).toEqual(6); - - nota = new Notation({ x: { y: [1] } }, { strict: true }); - expect(nota.get('x.y')).toEqual([1]); - expect(nota.get('x.z', 'default')).toEqual('default'); - expect(() => nota.get('x.y[1]')).toThrow('Implied index'); - expect(() => nota.get('x.z')).toThrow('Implied property'); - }); - - test('#set()', () => { - let nota = new Notation(_.cloneDeep(o)); - let obj = nota.value; - expect(obj.name).toEqual('onur'); - nota.set('name', 'cute'); - expect(obj.name).toEqual('cute'); - - // should not overwrite - nota.set('account.id', 120, false); - expect(obj.account.id).toEqual(15); - - // should overwrite - nota.set('account.id', 120, true); - expect(obj.account.id).toEqual(120); - - // should overwrite - nota.set('company.address.location.lat', 40.111111); - expect(obj.company.address.location.lat).toEqual(40.111111); - expect(obj.company.address.location.lon).toEqual(30.123123); - - nota.set('newProp.val', true); - expect(obj.newProp.val).toEqual(true); - - nota.set('account.newProp.val', 'YES'); - expect(obj.account.newProp.val).toEqual('YES'); - - nota.set('account.likes[1]', 'VFX'); - expect(obj.account.likes[1]).toEqual('VFX'); - expect(obj.account.likes).toEqual(expect.any(Array)); - - nota = create(); - nota.set('a.b.c[2].xyz[0]', 'value'); - obj = nota.value; - expect(obj.a.b.c[2].xyz[0]).toEqual('value'); - expect(obj.a.b.c[1]).toBeUndefined(); - expect(obj.a.b.c).toEqual(expect.any(Array)); - expect(obj.a.b.c[2].xyz).toEqual(expect.any(Array)); - - expect(() => create({}).set('', 1)).toThrow(); - expect(() => create({}).set(' ', 1)).toThrow(); - expect(() => create({}).set(null, 1)).toThrow(); - expect(() => create({}).set([], 1)).toThrow(); - // should throw if attempted to set anything other than an index on an - // array. - expect(() => create([]).set('x', true)).toThrow(); - expect(() => create([]).set(new Number(1), true)).toThrow(); // eslint-disable-line no-new-wrappers - expect(() => create({ x: { y: [] } }).set('x.y.z', true)).toThrow(); - - nota = new Notation({ x: { y: [1] } }); - expect(nota.set('x.y', 'value').value.x.y).toEqual('value'); - expect(nota.set('x.y', 'overwrite', false).value.x.y).toEqual('value'); - - // Cannot set value by inserting at index, on an object - expect(() => create().set('x', 1, 'insert')).toThrow(); - expect(() => create({ x: true }).set('x', 1, 'insert')).toThrow(); - }); - - test('#remove()', () => { - let nota = new Notation(_.cloneDeep(o)); - const obj = nota.value; - // console.log('before', o); - expect(obj.age).toBeDefined(); - nota.remove('age'); - expect(obj.age).toBeUndefined(); - - expect(obj.company.address.city).toEqual('istanbul'); - nota.remove('company.address.city'); - expect(obj.company.address.city).toBeUndefined(); - // console.log('after', o); - - // deleting non-existing property.. - // this should have no effect. - const k = Object.keys(obj.company).length; - nota.remove('company.noProp'); - expect(Object.keys(obj.company).length).toEqual(k); - - const assets = { boat: 'none', car: { model: 'Mustang' } }; - create(assets).delete('car.model'); // alias - expect(assets.car).toEqual({}); - - // expect(() => create([{ x: 1 }]).remove('x')).toThrow(); // TODO: strict option - expect(create({ x: { y: 1 } }).remove('x').value).toEqual({}); - expect(create({ x: { y: 1 } }).remove('x.y').value).toEqual({ x: {} }); - expect(() => create({ x: { y: 1 } }).remove('x.*').value).toThrow(); - expect(create([{ x: 1 }]).remove('[0]').value).toEqual([]); - - nota = new Notation({ x: { y: [1], z: 5 } }, { strict: true }); - expect(nota.remove('x.z').value.z).toBeUndefined(); - expect(() => nota.remove('x.y[2]')).toThrow('Implied index'); - expect(() => nota.remove('x.z')).toThrow('Implied property'); - }); - - test('#clone()', () => { - expect(create(o).clone().value).toEqual(o); - - const obj = { a: { b: { c: [1, 2, {}] } } }; - const clone = create(obj).clone().value; - expect(obj).toEqual(clone); - obj.x = true; - expect(clone.x).toBeUndefined(); - obj.a.b.c.push(3); - expect(clone.a.b.c.length).toEqual(3); - }); - - test('#merge(), #separate()', () => { - const nota = new Notation(_.cloneDeep(o)); - nota.merge({ - 'key': null, - 'newkey.p1': 13, - 'newkey.p2': false, - 'newkey.p3.val': [] - }); - const merged = nota.value; - // console.log(JSON.stringify(merged, null, ' ')); - expect(merged.key).toEqual(null); - expect(merged.newkey.p1).toEqual(13); - expect(merged.newkey.p2).toEqual(false); - expect(merged.newkey.p3.val).toEqual(jasmine.any(Array)); - - expect(() => create().merge(1)).toThrow(); - expect(() => create().merge([])).toThrow(); - expect(() => create().merge(null)).toThrow(); - - const separated = nota.separate(['newkey.p1', 'newkey.p2', 'newkey.p3.val']).value; - expect(separated.key).toBeUndefined(); - expect(separated.newkey.p1).toEqual(13); - expect(separated.newkey.p2).toEqual(false); - expect(separated.newkey.p3.val).toEqual(jasmine.any(Array)); - - expect(merged.key).toEqual(null); - expect(merged.newkey.p1).toBeUndefined(); - expect(merged.newkey.p2).toBeUndefined(); - expect(merged.newkey.p3.val).toBeUndefined(); - - expect(() => create().separate(1)).toThrow(); - expect(() => create().separate({})).toThrow(); - expect(() => create().separate(null)).toThrow(); - }); - - test('#rename()', () => { - const nota = new Notation(_.cloneDeep(o)); - const obj = nota.value; - expect(obj.company.address.location).toBeDefined(); - nota.rename('company.address.location', 'company.loc.geo'); - expect(obj.company.address.location).toBeUndefined(); - expect(obj.company.loc.geo.lat).toEqual(jasmine.any(Number)); - - expect(obj.name).toBeDefined(); - nota.rename('name', 'person'); - expect(obj.name).toBeUndefined(); - expect(obj.person).toBeDefined(); - nota.renote('person', 'me.name'); // alias - expect(obj.person).toBeUndefined(); - expect(obj.me.name).toBeDefined(); - - expect(() => nota.rename('', 'test')).toThrow(); - expect(() => nota.rename('company', '')).toThrow(); - }); - - test('#copyToNew(), alias: #extract()', () => { - const nota = new Notation(_.cloneDeep(o)); - const obj = nota.value; - // `extract(notation)` is same as `copyTo({}, notation)` - let ex; - ex = nota.extract('company'); - expect(obj.company.name).toEqual('pilot co'); - expect(ex.company.name).toEqual('pilot co'); - ex = nota.copyToNew('company.address.country'); // alias - expect(obj.company.address.country).toEqual('TR'); - expect(ex.company.address.country).toEqual('TR'); - }); - - test('#copyFrom()', () => { - const nota = new Notation(); - const src = { a: [{ x: 1 }], b: 2 }; - nota.copyFrom(src, 'a[0].x'); - expect(nota.value.a[0].x).toEqual(1); - nota.copyFrom(src, 'b', 'a[1]'); - expect(nota.value.a[1]).toEqual(2); - expect(nota.value.b).toBeUndefined(); - nota.copyFrom(src, 'none', null, true); - expect('none' in nota.value).toEqual(false); - - expect(src).toEqual({ a: [{ x: 1 }], b: 2 }); - - expect(() => nota.copyFrom(5, 'toString')).toThrow(); - }); - - test('#moveToNew(), alias: #extrude()', () => { - const nota = new Notation(_.cloneDeep(o)); - const obj = nota.value; - // `extrude(notation)` is same as `moveTo({}, notation)` - let ex; - ex = nota.extrude('company.address.country'); - expect(obj.company.address.country).toBeUndefined(); - expect(ex.company.address.country).toEqual('TR'); - ex = nota.moveToNew('company', 'comp.my'); // alias - expect(obj.company).toBeUndefined(); - expect(ex.company).toBeUndefined(); - expect(ex.comp.my.name).toEqual('pilot co'); - - expect(() => nota.moveTo(5, 'company')).toThrow(); - }); - - test('#moveFrom()', () => { - const nota = new Notation(); - const src = { a: [{ x: 1 }], b: 2 }; - nota.moveFrom(src, 'a[0].x'); - expect(nota.value.a[0].x).toEqual(1); - nota.moveFrom(src, 'b', 'a[1]'); - expect(nota.value.a[1]).toEqual(2); - expect(nota.value.b).toBeUndefined(); - nota.moveFrom(src, 'none', null, true); - expect('none' in nota.value).toEqual(false); - - expect(src).toEqual({ a: [{}] }); - - expect(() => nota.moveFrom(5, 'toString')).toThrow(); - }); - - test('return `undefined` for invalid notations', () => { - const nota = new Notation(_.cloneDeep(o)); - const level1 = 'noProp'; - const level2 = 'noProp.level2'; - expect(nota.get(level1)).toBeUndefined(); - expect(nota.hasDefined(level1)).toEqual(false); - expect(nota.get(level2)).toBeUndefined(); - expect(nota.hasDefined(level2)).toEqual(false); - }); - - test('ignore invalid notations', () => { - const nota = new Notation(_.cloneDeep(o)); - const obj = nota.value; - let ex; - const level1 = 'noProp'; - const level2 = 'noProp.level2'; - - ex = nota.extract(level1); - expect(nota.get(level1)).toBeUndefined(); - expect(Object.keys(ex).length).toEqual(0); - - ex = nota.extract(level2); - expect(nota.get(level2)).toBeUndefined(); - expect(Object.keys(ex).length).toEqual(0); - - ex = nota.extrude(level1); - expect(nota.get(level1)).toBeUndefined(); - expect(Object.keys(ex).length).toEqual(0); - - ex = nota.extrude(level2); - expect(nota.get(level2)).toBeUndefined(); - expect(Object.keys(ex).length).toEqual(0); - - expect(obj.account.noProp).toBeUndefined(); - nota.rename('account.noProp', 'renamedProp'); - expect(obj.renamedProp).toBeUndefined(); - }); - - test('throw if invalid object or notation', () => { - expect(() => new Notation(null)).toThrow(); - expect(() => new Notation(undefined)).toThrow(); - expect(() => new Notation(new Date())).toThrow(); - expect(() => new Notation(Number)).toThrow(); - expect(() => create(1)).toThrow(); - expect(() => create(true)).toThrow(); - expect(() => create('no')).toThrow(); - - const nota = new Notation(_.cloneDeep(o)); - expect(() => nota.copyTo(null, 'account')).toThrow(); - expect(() => nota.has({}, 4)).toThrow(); - expect(() => nota.remove({})).toThrow(); - expect(() => nota.rename('account', {})).toThrow(); - expect(() => nota.get('prop2')).not.toThrow(); - }); - - test('property names', () => { - const a = { - 'x': 1, - 'y': { c: 2 }, - '@test': true - }; - const notation = new Notation(a); - expect(notation.set('y.c', 5).value.y.c).toEqual(5); - expect(notation.set('["@test"]', false).value['@test']).toEqual(false); - }); - -}); diff --git a/test/utils.spec.js b/test/utils.spec.js deleted file mode 100644 index a442b21..0000000 --- a/test/utils.spec.js +++ /dev/null @@ -1,197 +0,0 @@ -/* eslint camelcase:0, consistent-return:0, max-lines-per-function:0 */ - -import { utils } from '../src/utils'; - -describe('utils', () => { - - test('.type(), .ensureArray()', () => { - expect(utils.type({})).toEqual('object'); - expect(utils.type([])).toEqual('array'); - expect(utils.type(null)).toEqual('null'); - expect(utils.type(undefined)).toEqual('undefined'); - expect(utils.type(new Error())).toEqual('error'); - expect(utils.type(new Date())).toEqual('date'); - - expect(utils.ensureArray(null)).toEqual([]); - expect(utils.ensureArray(undefined)).toEqual([]); - expect(utils.ensureArray(true)).toEqual([true]); - expect(utils.ensureArray(false)).toEqual([false]); - expect(utils.ensureArray([null])).toEqual([null]); - expect(utils.ensureArray(1)).toEqual([1]); - expect(utils.ensureArray('str')).toEqual(['str']); - }); - - test('.hasOwn(), .cloneDeep()', () => { - expect(utils.hasOwn({ a: 1 }, 'a')).toEqual(true); - expect(utils.hasOwn({ a: 1 }, 'b')).toEqual(false); - expect(utils.hasOwn({}, 'hasOwnProperty')).toEqual(false); - expect(utils.hasOwn({ hasOwnProperty: () => true }, 'x')).toEqual(false); - function Obj() {} // eslint-disable-line - Obj.prototype.hasOwnProperty = () => true; - expect(utils.hasOwn(new Obj(), 'x')).toEqual(false); - expect(utils.hasOwn(['0', 'a'], 0)).toEqual(true); - expect(utils.hasOwn(['0', 'a'], '0')).toEqual(false); - expect(utils.hasOwn(['2', 'a'], 2)).toEqual(false); - - expect(utils.cloneDeep({})).toEqual({}); - expect(utils.cloneDeep(null)).toEqual(null); - const o = { a: { b: { c: [1, { o: 2 }, 3] }, x: true, y: { d: 'e', f: 4 } }, z: 5 }; - let copy = utils.cloneDeep(o); - expect(copy).toEqual(o); - expect(copy === o).toEqual(false); - copy = utils.cloneDeep([o]); - expect(copy).toEqual([o]); - expect(copy === [o]).toEqual(false); - }); - - test('.each(), .eachRight()', () => { - const a = [1, 2, 3, 4]; - - let out = []; - utils.each(a, (value, index, list) => { - expect(value).toEqual(a[index]); - expect(index).toEqual(a[index] - 1); - expect(a).toEqual(list); - out.push(value); - }); - expect(out).toEqual(a); - - out = []; - utils.eachRight(a, (value, index, list) => { - expect(value).toEqual(a[index]); - expect(index).toEqual(a[index] - 1); - expect(a).toEqual(list); - out.push(value); - }); - expect(out).toEqual(a.reverse()); - - // break out / return early test - out = []; - utils.each(a, (value, index) => { - if (index <= 1) { - out.push(value); - } else { - return false; - } - }); - expect(out.length).toEqual(2); - - out = []; - utils.eachRight(a, (value, index) => { - if (index > 1) { - out.push(value); - } else { - return false; - } - }); - expect(out.length).toEqual(2); - }); - - test('.eachItem()', () => { - const c1 = [1, 'a', true, { x: [2] }, [3]]; - let out = []; - utils.eachItem(c1, (item, index, collection) => { - out.push([index, item]); - expect(collection).toEqual(c1); - }); - expect(out).toEqual([[0, 1], [1, 'a'], [2, true], [3, { x: [2] }], [4, [3]]]); - - const c2 = { a: 1, b: true, c: [2], d: { e: 3 }, f: 'f' }; - out = []; - utils.eachItem(c2, (item, key, collection) => { - out.push([key, item]); - expect(collection).toEqual(c2); - }); - expect(out).toEqual([ - ['a', 1], - ['b', true], - ['c', [2]], - ['d', { e: 3 }], - ['f', 'f'] - ]); - }); - - test('.stringOrArrayOf(), .hasSingleItemOf()', () => { - expect(utils.stringOrArrayOf('test', 'test')).toEqual(true); - expect(utils.stringOrArrayOf(['test'], 'test')).toEqual(true); - expect(utils.stringOrArrayOf(['test'], 'x')).toEqual(false); - expect(utils.stringOrArrayOf([1], 1)).toEqual(false); // should be string - - expect(utils.hasSingleItemOf(['test'], 'test')).toEqual(true); - expect(utils.hasSingleItemOf(['t'])).toEqual(true); - expect(utils.hasSingleItemOf(['t'], 't')).toEqual(true); - expect(utils.hasSingleItemOf(['t'], 'x')).toEqual(false); - expect(utils.hasSingleItemOf([1], 1)).toEqual(true); - }); - - test('.pregQuote()', () => { - expect(utils.pregQuote('*')).toEqual('\\*'); - expect(utils.pregQuote('[.+]')).toEqual('\\[\\.\\+\\]'); - expect(utils.pregQuote('[a-z]')).toEqual('\\[a\\-z\\]'); - expect(utils.pregQuote('(?:1|2)')).toEqual('\\(\\?\\:1\\|2\\)'); - expect(utils.pregQuote('x y z 1 2 3')).toEqual('x y z 1 2 3'); - }); - - test('.normalizeNote()', () => { - expect(utils.normalizeNote('a')).toEqual('a'); - expect(() => utils.normalizeNote('a.b')).toThrow(); - expect(() => utils.normalizeNote('[a.b]')).toThrow(); - expect(() => utils.normalizeNote('["a.b"]')).not.toThrow(); - - expect(utils.normalizeNote('[1]')).toEqual(1); - expect(() => utils.normalizeNote('[1.1]')).toThrow(); - expect(() => utils.normalizeNote('[-1]')).toThrow(); - - expect(utils.normalizeNote('["-1"]')).toEqual('-1'); - expect(utils.normalizeNote('["1"]')).toEqual('1'); - expect(utils.normalizeNote('["[x]"]')).toEqual('[x]'); - expect(utils.normalizeNote('["x.y"]')).toEqual('x.y'); - - expect(() => utils.normalizeNote('[]')).toThrow(); - // obj[''] = value is allowed in JS - expect(() => utils.normalizeNote('[""]')).not.toThrow(); - // but cannot be represented without brackets - expect(() => utils.normalizeNote('')).toThrow(); - }); - - test('.removeTrailingWildcards()', () => { - expect(utils.removeTrailingWildcards('*[*]')).toEqual('*'); - expect(utils.removeTrailingWildcards('[*].*')).toEqual('[*]'); - expect(utils.removeTrailingWildcards('*[*].*[*]')).toEqual('*'); - expect(utils.removeTrailingWildcards('*[*].*[*].*')).toEqual('*'); - expect(utils.removeTrailingWildcards('!*[*].*[*].*')).toEqual('!*[*].*[*].*'); - expect(utils.removeTrailingWildcards('*[*].*[*].*.x')).toEqual('*[*].*[*].*.x'); - expect(utils.removeTrailingWildcards('x.*[*].*[*].*')).toEqual('x'); - expect(utils.removeTrailingWildcards('[*].*[*].*')).toEqual('[*]'); - expect(utils.removeTrailingWildcards('[*].*[*].*[*]')).toEqual('[*]'); - expect(utils.removeTrailingWildcards('![*].*[*].*[*]')).toEqual('![*].*[*].*[*]'); - expect(utils.removeTrailingWildcards('[*].*[*].*[*].x')).toEqual('[*].*[*].*[*].x'); - expect(utils.removeTrailingWildcards('x[*].*[*].*[*]')).toEqual('x'); - }); - - test('.cloneDeep()', () => { - const now = Date.now(); - const original = { - str: 'string', - num: 1, - bool: true, - date: new Date(now), - regexp: /abc/i, - arr: [1, { a: 2, b: 3 }, [4, 5]], - obj: { x: 1, y: { z: true } }, - nil: null, - undef: undefined - }; - let cloned = utils.cloneDeep(original); - expect(original).toEqual(cloned); - - // symbols are unique, so won't be exact but values should match - original.symbol = Symbol('test'); - cloned = utils.cloneDeep(original); - expect(original.symbol.valueOf()).toEqual(cloned.symbol.valueOf()); - - original.circular = original; - expect(() => utils.cloneDeep(original)).toThrow(); - }); - -}); diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index 7c2f10e..0000000 --- a/webpack.config.js +++ /dev/null @@ -1,100 +0,0 @@ -const path = require('path'); -const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); - -const libraryName = 'notation'; -const libPath = path.resolve(__dirname, 'lib'); -const srcPath = path.resolve(__dirname, 'src'); -const publicPath = 'lib/'; - -module.exports = env => { - - const config = { - // turn off NodeStuffPlugin and NodeSourcePlugin plugins. Otherwise - // objects like `process` are mocked or polyfilled. - node: false, // !!! IMPORTANT - - context: __dirname, - cache: false, - entry: path.join(srcPath, 'index.js'), - devtool: 'source-map', - output: { - library: libraryName, - filename: libraryName.toLowerCase() + '.js' - }, - module: { - rules: [ - { - test: /(\.jsx?)$/, - loader: 'babel-loader', - query: { - presets: ['@babel/preset-env'] - }, - exclude: /(node_modules|bower_components)/ - }, - { - test: /\.html?$/, - loader: 'html-loader' - } - ] - }, - resolve: { - modules: [srcPath], - extensions: ['.js'] - }, - // Configure the console output. - stats: { - colors: true, - modules: false, - reasons: true, - // suppress "export not found" warnings about re-exported types - warningsFilter: /export .* was not found in/ - }, - plugins: [], - optimization: { - minimizer: [] - } - }; - - if (env.WEBPACK_OUT === 'coverage') { - Object.assign(config.output, { - filename: '.' + libraryName + '.cov.js', - path: libPath, - libraryTarget: 'commonjs2', - umdNamedDefine: false - }); - } else { - - // production & development - Object.assign(config.output, { - path: libPath, - // filename: libraryName.toLowerCase() + '.js', - publicPath, - libraryTarget: 'umd', - umdNamedDefine: true, - // this is to get rid of 'window is not defined' error. - // https://stackoverflow.com/a/49119917/112731 - globalObject: 'this' - }); - - if (env.WEBPACK_OUT === 'production') { - config.devtool = 'source-map'; - config.output.filename = libraryName.toLowerCase() + '.min.js'; - config.optimization.minimizer.push(new UglifyJsPlugin({ - test: /\.js$/, - sourceMap: true, - uglifyOptions: { - ie8: false, - ecma: 5, - output: { - comments: false, - beautify: false - }, - compress: true, - warnings: true - } - })); - } - } - - return config; -}; From 55ec08383b99d40bf32df7ad434e034350f1c9cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20Y=C4=B1ld=C4=B1r=C4=B1m?= Date: Mon, 15 Jun 2026 20:18:22 +0300 Subject: [PATCH 02/17] update project config --- .editorconfig | 6 +----- .gitignore | 15 +++++++++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.editorconfig b/.editorconfig index 620c529..4a7ea30 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,15 +2,11 @@ root = true [*] indent_style = space -indent_size = 4 +indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -[{package.json,*.xml}] -indent_style = space -indent_size = 2 - [*.md] trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore index 74dc521..1d38e70 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,15 @@ -backup -coverage -wiki node_modules +package-lock.json +lib +coverage +test/coverage +test/stryker +.stryker-tmp +*.tsbuildinfo npm-debug.log +backup +wiki tmp TODO.md -.vscode/ \ No newline at end of file +.vscode/ +.DS_Store From 0117afadb32f2d43c6ec1762826438b16bbff2b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20Y=C4=B1ld=C4=B1r=C4=B1m?= Date: Mon, 15 Jun 2026 20:18:43 +0300 Subject: [PATCH 03/17] added github ci workflow --- .github/workflows/ci.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..79fe5a6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: [20, 22, 24] + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm run build + - run: npm test From da9e1b0efa993b8c30af54ab22d844a282aaaf35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20Y=C4=B1ld=C4=B1r=C4=B1m?= Date: Mon, 15 Jun 2026 20:18:59 +0300 Subject: [PATCH 04/17] Create biome.json --- biome.json | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 biome.json diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..a335725 --- /dev/null +++ b/biome.json @@ -0,0 +1,57 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.5.0/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "includes": ["src/**/*.ts", "test/**/*.ts", "*.ts"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "noExplicitAny": "off", + "noImplicitAnyLet": "off", + "noConfusingVoidType": "off", + "noSparseArray": "off" + }, + "style": { + "useTemplate": "off", + "noNonNullAssertion": "off", + "noParameterAssign": "off" + }, + "complexity": { + "noBannedTypes": "off", + "noArguments": "off" + }, + "correctness": { + "noUnusedFunctionParameters": "off", + "noUnusedPrivateClassMembers": "off", + "noVoidTypeReturn": "off" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "semicolons": "always", + "trailingCommas": "none" + } + }, + "assist": { + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} From 5b5edb50bd91b2e16126d8339b14b2266c1128e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20Y=C4=B1ld=C4=B1r=C4=B1m?= Date: Mon, 15 Jun 2026 20:19:07 +0300 Subject: [PATCH 05/17] Create vitest.config.ts --- vitest.config.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 vitest.config.ts diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..7a096c2 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['test/**/*.spec.ts'], + coverage: { + provider: 'istanbul', + reporter: ['text', 'lcov'], + reportsDirectory: 'test/coverage', + include: ['src/**/*.ts'], + thresholds: { + lines: 100, + functions: 100, + statements: 100, + branches: 100 + } + } + } +}); From d30ed482c93c98f8a10eb3509d99218d834b7b1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20Y=C4=B1ld=C4=B1r=C4=B1m?= Date: Mon, 15 Jun 2026 20:19:42 +0300 Subject: [PATCH 06/17] added tsconfig --- tsconfig.build.json | 10 ++++++++++ tsconfig.json | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..82c3cb9 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "types": ["node"] + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9e49e81 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "tsconfig-oy", + "compilerOptions": { + "ignoreDeprecations": "6.0", + "isolatedModules": true, + "verbatimModuleSyntax": true, + "incremental": false, + "outDir": "lib", + "declarationDir": "lib", + "declarationMap": true, + "rootDir": ".", + "typeRoots": ["node_modules/@types"], + "types": ["node", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} From 9eb8504f3ab82964749655dd7d99f20de32451b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20Y=C4=B1ld=C4=B1r=C4=B1m?= Date: Mon, 15 Jun 2026 20:20:02 +0300 Subject: [PATCH 07/17] added tests --- test/Notation.spec.ts | 726 ++++++++++++++++++++++++ test/NotationError.spec.ts | 42 ++ test/NotationGlob.spec.ts | 1068 +++++++++++++++++++++++++++++++++++ test/ac.spec.ts | 82 +++ test/bracket.spec.ts | 358 ++++++++++++ test/edge.spec.ts | 60 ++ test/errors.spec.ts | 43 ++ test/filter.spec.ts | 431 ++++++++++++++ test/utils.spec.ts | 204 +++++++ test/utils.targeted.spec.ts | 86 +++ 10 files changed, 3100 insertions(+) create mode 100644 test/Notation.spec.ts create mode 100644 test/NotationError.spec.ts create mode 100644 test/NotationGlob.spec.ts create mode 100644 test/ac.spec.ts create mode 100644 test/bracket.spec.ts create mode 100644 test/edge.spec.ts create mode 100644 test/errors.spec.ts create mode 100644 test/filter.spec.ts create mode 100644 test/utils.spec.ts create mode 100644 test/utils.targeted.spec.ts diff --git a/test/Notation.spec.ts b/test/Notation.spec.ts new file mode 100644 index 0000000..c384007 --- /dev/null +++ b/test/Notation.spec.ts @@ -0,0 +1,726 @@ +/* eslint camelcase:0, max-lines-per-function:0, consistent-return:0, max-statements:0, max-lines:0 */ + +import { Notation } from '../src/core/Notation.js'; +import { NotationError } from '../src/core/NotationError.js'; +import type { UnknownObject } from '../src/types.js'; + +const o = { + name: 'onur', + age: 36, + account: { + id: 15, + tags: 20, + likes: ['movies', '3d', 'programming', 'music'] + }, + billing: { + account: { + id: 121, + credit: 300, + balance: -293 + } + }, + company: { + name: 'pilot co', + address: { + city: 'istanbul', + country: 'TR', + location: { + lat: 34.123123, + lon: 30.123123 + } + }, + account: { + id: 33, + taxNo: 12345 + }, + limited: true, + notDefined: undefined, + nuller: null, + zero: 0 + } +}; + +describe('Notation', () => { + const { create, first, last, parent, countNotes, isValid, split, join, eachNote } = Notation; + + // beforeEach(() => { }); + + test('NotationError', () => { + const errorMessage = 'TEST_ERROR'; + try { + throw new NotationError(errorMessage); + } catch (error) { + // console.log(error); + expect((error as Error).name).toEqual('NotationError'); + expect((error as Error).message).toEqual(errorMessage); + expect(error instanceof Error).toEqual(true); + expect(error instanceof NotationError).toEqual(true); + expect(error instanceof NotationError).toEqual(true); + expect(Object.prototype.toString.call(error)).toEqual('[object Error]'); + expect(Object.getPrototypeOf(error)).toEqual(NotationError.prototype); + } + expect(new NotationError() instanceof NotationError).toEqual(true); + }); + + test('.first(), .last(), .parent()', () => { + let notation = 'first.mid.last'; + expect(first(notation)).toEqual('first'); + expect(last(notation)).toEqual('last'); + expect(parent(notation)).toEqual('first.mid'); + + notation = 'single'; + expect(first(notation)).toEqual('single'); + expect(last(notation)).toEqual('single'); + expect(parent(notation)).toEqual(null); + }); + + test('.countNotes()', () => { + expect(countNotes('a')).toEqual(1); + expect(countNotes('a.b')).toEqual(2); + expect(countNotes('a.b.c')).toEqual(3); // alias + expect(() => countNotes('')).toThrow(); // eslint-disable-line + }); + + test('.isValid()', () => { + expect(isValid('first.mid.last')).toEqual(true); + expect(isValid('first.mid.')).toEqual(false); + expect(isValid('first.')).toEqual(false); + expect(isValid('first')).toEqual(true); + expect(isValid('.first')).toEqual(false); + expect(isValid('.')).toEqual(false); + // @ts-expect-error: testing null + expect(isValid(null)).toEqual(false); + // @ts-expect-error: testing boolean + expect(isValid(true)).toEqual(false); + // star is NOT treated as wildcard here. this is normal dot-notation, + // not a glob. + expect(isValid('prop.*')).toEqual(false); + expect(isValid('prop["*"]')).toEqual(true); + }); + + test('.split(), .join()', () => { + expect(split('a')).toEqual(['a']); + expect(split('[3]')).toEqual(['[3]']); + expect(split('[10].x.y[1].a')).toEqual(['[10]', 'x', 'y', '[1]', 'a']); + expect(split('a.b[0].x.y["5"].z')).toEqual(['a', 'b', '[0]', 'x', 'y', '["5"]', 'z']); + expect(split('a.b[0][1][0]')).toEqual(['a', 'b', '[0]', '[1]', '[0]']); + expect(() => split('')).toThrow(); + expect(() => split('.')).toThrow(); + expect(() => split(' . ')).toThrow(); + expect(() => split('[]')).toThrow(); + expect(() => split('.b')).toThrow(); + expect(() => split('a-b')).toThrow(); + expect(() => split('["a-b"]')).not.toThrow(); + // @ts-expect-error: testing number + expect(() => split(1)).toThrow(); + // @ts-expect-error: testing object + expect(() => split({})).toThrow(); + // @ts-expect-error: testing array + expect(() => split([])).toThrow(); + // @ts-expect-error: testing null + expect(() => split(null)).toThrow(); + // @ts-expect-error: testing undefined + expect(() => split(undefined)).toThrow(); + + expect(join(['[10]', 'x', 'y', '[1]', 'a'])).toEqual('[10].x.y[1].a'); + expect(join(['a', 'b', '[0]', 'x', 'y', '["5"]', 'z'])).toEqual('a.b[0].x.y["5"].z'); + expect(join(['a', 'b', '[0]', '[1]', '[0]'])).toEqual('a.b[0][1][0]'); + }); + + test('.eachNote()', () => { + const pattern = 'arr[0].x[1][0].y[5]'; + const result = { + 0: { levelNota: 'arr', note: 'arr' }, + 1: { levelNota: 'arr[0]', note: '[0]' }, + 2: { levelNota: 'arr[0].x', note: 'x' }, + 3: { levelNota: 'arr[0].x[1]', note: '[1]' }, + 4: { levelNota: 'arr[0].x[1][0]', note: '[0]' }, + 5: { levelNota: 'arr[0].x[1][0].y', note: 'y' }, + 6: { levelNota: 'arr[0].x[1][0].y[5]', note: '[5]' } + }; + let c = 0; + eachNote(pattern, (levelNota, note, index) => { + const expected = result[index]; + expect(levelNota).toEqual(expected.levelNota); + expect(note).toEqual(expected.note); + c++; + }); + expect(c).toEqual(countNotes(pattern)); + + // alias test + c = 0; + eachNote(pattern, (levelNota, note, index) => { + const expected = result[index]; + expect(levelNota).toEqual(expected.levelNota); + expect(note).toEqual(expected.note); + c++; + }); + expect(c).toEqual(countNotes(pattern)); + }); + + test('constructor(), .create()', () => { + expect(new Notation().set('x', 'value').value.x).toEqual('value'); + expect(new Notation({}).set('x', '123').value.x).toEqual('123'); + // { length: 0 } + expect(new Notation([]).set('[0]', 'value').value[0]).toEqual('value'); + + expect(() => new Notation(undefined)).toThrow(); + // @ts-expect-error: testing null + expect(() => new Notation(null)).toThrow(); + // @ts-expect-error: testing number + expect(() => new Notation(1)).toThrow(); + // @ts-expect-error: testing string + expect(() => new Notation('test')).toThrow(); + + expect(create().set('x', 'value').value.x).toEqual('value'); + expect(create({}).set('x', 'value').value.x).toEqual('value'); + expect(create([]).set('[0]', 'value').value[0]).toEqual('value'); + + let nota = create(); + expect(nota.options.strict).toEqual(false); + expect(nota.options.preserveIndices).toEqual(false); + nota = create([], { strict: true, preserveIndices: true }); + expect(nota.options.strict).toEqual(true); + expect(nota.options.preserveIndices).toEqual(true); + + expect(() => create(undefined)).not.toThrow(); + // @ts-expect-error: testing null + expect(() => create(null)).not.toThrow(); + // @ts-expect-error: testing number + expect(() => create(1)).toThrow(); + // @ts-expect-error: testing string + expect(() => create('test')).toThrow(); + }); + + test('#flatten(), #exapand()', () => { + const nota = new Notation(structuredClone(o)); + const flat = nota.flatten().value; + + // console.log(flat); + expect(flat.name).toEqual(o.name); + expect(flat['account.id']).toEqual(o.account.id); + expect(flat['account.likes']).toBeUndefined(); + expect(flat['account.likes[0]']).toEqual(o.account.likes[0]); + expect(flat['account.likes[1]']).toEqual(o.account.likes[1]); + expect(flat['account.likes[2]']).toEqual(o.account.likes[2]); + expect(flat['account.likes[3]']).toEqual(o.account.likes[3]); + expect(flat['company.name']).toEqual(o.company.name); + expect(flat['company.address.location.lat']).toEqual(o.company.address.location.lat); + + const expanded = create(flat).expand().value; + expect(expanded.name).toEqual(o.name); + expect(expanded.account.id).toEqual(o.account.id); + expect(expanded.account.likes).toEqual(expect.any(Array)); + expect(expanded.company.name).toEqual(o.company.name); + expect(expanded.company.address.location.lat).toEqual(o.company.address.location.lat); + expect(expanded.account.likes.length).toEqual(4); + }); + + test('#each(), #eachValue()', () => { + const assets = { + boat: 'none', + car: { + brand: 'Ford', + model: 'Mustang', + year: 1970, + color: 'red', + other: { + model: null, + colors: ['blue', 'white'], + years: [1965, 1967] + }, + alternate: { + cars: [{ brand: 'Dodge', model: 'Charger', year: 1970 }] + } + } + }; + let nota = create(assets); + let result: string[] = []; + nota.each((notation) => { + result.push(notation); + }); + const expected = [ + 'boat', + 'car.brand', + 'car.model', + 'car.year', + 'car.color', + 'car.other.model', + 'car.other.colors[0]', + 'car.other.colors[1]', + 'car.other.years[0]', + 'car.other.years[1]', + 'car.alternate.cars[0].brand', + 'car.alternate.cars[0].model', + 'car.alternate.cars[0].year' + ]; + expect(result).toEqual(expected); + + result = []; + nota.each((notation) => { + result.push(notation); + return result.length < 4 ? undefined : false; // should break out on 4 + }); + expect(result.length).toEqual(4); + + let c = 0; + nota.eachValue('car.alternate.cars[0]', (levelValue, _levelNotation, _note, index) => { + c++; + if (index === 0) expect((levelValue as UnknownObject).model).toEqual('Mustang'); + if (index === 1) return false; + return; + }); + expect(c).toEqual(2); + + // Test function instance / prototype + + function A() {} // eslint-disable-line + A.prototype.x = 1; // x is NOT own enumerable + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const a = new (A as any)(); + nota = new Notation(a); + c = 0; + nota.eachValue('x', (levelValue, levelNotation, note, index) => { + c++; + if (index === 0) expect(levelValue).toBeUndefined(); + }); + expect(c).toEqual(1); + + // Test class instance property + + class B { + x: number = 1; + } // x is own enumerable + nota = new Notation(new B()); + c = 0; + nota.eachValue('x', (levelValue, levelNotation, note, index) => { + c++; + if (index === 0) expect(levelValue).toBeDefined(); + }); + expect(c).toEqual(1); + }); + + test('#getNotations()', () => { + const obj = { a: { b: { c: [{ x: 1 }], d: 2 }, e: 3, f: 4 }, g: [4, [5]] }; + const notations = create(obj).getNotations(); + expect(notations).toEqual(['a.b.c[0].x', 'a.b.d', 'a.e', 'a.f', 'g[0]', 'g[1][0]']); + }); + + test('#inspectGet(), #inspectRemove()', () => { + const obj = { a: { b: [{ c: 1 }] }, d: undefined, e: null, f: [1, false, 2] }; + let nota = create(obj).clone(); + + let ins = nota.inspectGet('a.b[0]'); + expect(ins.notation).toEqual('a.b[0]'); + expect(ins.has).toEqual(true); + expect(ins.value).toEqual({ c: 1 }); + expect(ins.lastNote).toEqual('[0]'); + expect(ins.lastNoteNormalized).toEqual(0); + + ins = nota.inspectGet('d'); + expect(ins.notation).toEqual('d'); + expect(ins.has).toEqual(true); + expect(ins.value).toEqual(undefined); + expect(ins.lastNote).toEqual('d'); + expect(ins.lastNoteNormalized).toEqual('d'); + + ins = nota.inspectRemove('e'); + expect(ins.notation).toEqual('e'); + expect(ins.has).toEqual(true); + expect(ins.value).toEqual(null); + expect(ins.lastNote).toEqual('e'); + expect(ins.lastNoteNormalized).toEqual('e'); + // original should not be mutated + expect(obj.e).toEqual(null); + + ins = nota.inspectRemove('f[1]'); + expect(ins.notation).toEqual('f[1]'); + expect(ins.has).toEqual(true); + expect(ins.value).toEqual(false); + expect(ins.lastNote).toEqual('[1]'); + expect(ins.lastNoteNormalized).toEqual(1); + expect(nota.value.f[1]).toEqual(2); + expect(nota.value.f.length).toEqual(2); + // original should not be mutated + expect(obj.f[1]).toEqual(false); + expect(obj.f.length).toEqual(3); + + let arr = ['a', 'b', 'c']; + nota = create(arr, { preserveIndices: false }); + expect(nota.options.preserveIndices).toEqual(false); + ins = nota.inspectRemove('[1]'); + expect(ins.value).toEqual('b'); + expect(arr[1]).toEqual('c'); + expect(arr.length).toEqual(2); + + arr = ['a', 'b', 'c']; + nota = create(arr, { preserveIndices: true }); + expect(nota.options.preserveIndices).toEqual(true); + ins = nota.inspectRemove('[1]'); + expect(ins.value).toEqual('b'); + expect(arr[1]).toEqual(undefined); + expect(arr.length).toEqual(3); + }); + + test('#has(), #hasDefined()', () => { + const nota = new Notation(structuredClone(o)); + // console.info('....... nota >>>', nota.value); + expect(nota.has('name')).toEqual(true); + expect(nota.has('company.address.location.lat')).toEqual(true); + expect(nota.has('company.notDefined')).toEqual(true); + expect(nota.has('notProp1')).toEqual(false); + expect(nota.has('company.notProp2')).toEqual(false); + + expect(nota.hasDefined('name')).toEqual(true); + expect(nota.hasDefined('account.id')).toEqual(true); + expect(nota.hasDefined('company.address.location.lat')).toEqual(true); + expect(nota.hasDefined('company.notDefined')).toEqual(false); + expect(nota.hasDefined('company.none')).toEqual(false); + expect(nota.hasDefined('company.nuller')).toEqual(true); + expect(nota.hasDefined('company.zero')).toEqual(true); + expect(nota.hasDefined('notProp1')).toEqual(false); + expect(nota.hasDefined('company.notProp2')).toEqual(false); + }); + + test('#get()', () => { + let nota = new Notation(structuredClone(o)); + expect(nota.get('name')).toEqual(o.name); + expect(nota.get('account.id')).toEqual(o.account.id); + expect(nota.get('company.address.location.lat')).toEqual(o.company.address.location.lat); + expect(nota.get('account.noProp')).toBeUndefined(); + + nota = new Notation({ a: { b: { c: [null, { d: { e: ['value'] } }, true] } } }); + expect(nota.get('a.b.c')).toEqual(expect.any(Array)); + expect(nota.get('a.b.c[1].d.e')).toEqual(['value']); + expect(nota.get('a.b.c[2]')).toEqual(true); + + const data = { + obj: { a: { b: true } }, + arr: [ + { x: { y: 8 }, z: true }, + { x: { y: 4 }, z: true }, + { x: { y: 6 }, z: true } + ] + }; + nota = new Notation(data); + expect(nota.get('arr[0].x')).toEqual({ y: 8 }); + expect(nota.get('arr[0].x.y')).toEqual(8); + expect(nota.get('arr[1].x')).toEqual({ y: 4 }); + expect(nota.get('arr[1].x.y')).toEqual(4); + expect(nota.get('arr[2].x')).toEqual({ y: 6 }); + expect(nota.get('arr[2].x.y')).toEqual(6); + + nota = new Notation({ x: { y: [1] } }, { strict: true }); + expect(nota.get('x.y')).toEqual([1]); + expect(nota.get('x.z', 'default')).toEqual('default'); + expect(() => nota.get('x.y[1]')).toThrow('Implied index'); + expect(() => nota.get('x.z')).toThrow('Implied property'); + }); + + test('#set()', () => { + let nota = new Notation(structuredClone(o)); + let obj = nota.value; + expect(obj.name).toEqual('onur'); + nota.set('name', 'cute'); + expect(obj.name).toEqual('cute'); + + // should not overwrite + nota.set('account.id', 120, false); + expect(obj.account.id).toEqual(15); + + // should overwrite + nota.set('account.id', 120, true); + expect(obj.account.id).toEqual(120); + + // should overwrite + nota.set('company.address.location.lat', 40.111111); + expect(obj.company.address.location.lat).toEqual(40.111111); + expect(obj.company.address.location.lon).toEqual(30.123123); + + nota.set('newProp.val', true); + expect(obj.newProp.val).toEqual(true); + + nota.set('account.newProp.val', 'YES'); + expect(obj.account.newProp.val).toEqual('YES'); + + nota.set('account.likes[1]', 'VFX'); + expect(obj.account.likes[1]).toEqual('VFX'); + expect(obj.account.likes).toEqual(expect.any(Array)); + + nota = create(); + nota.set('a.b.c[2].xyz[0]', 'value'); + obj = nota.value; + expect(obj.a.b.c[2].xyz[0]).toEqual('value'); + expect(obj.a.b.c[1]).toBeUndefined(); + expect(obj.a.b.c).toEqual(expect.any(Array)); + expect(obj.a.b.c[2].xyz).toEqual(expect.any(Array)); + + expect(() => create({}).set('', 1)).toThrow(); + expect(() => create({}).set(' ', 1)).toThrow(); + // @ts-expect-error: testing null + expect(() => create({}).set(null, 1)).toThrow(); + // @ts-expect-error: testing array + expect(() => create({}).set([], 1)).toThrow(); + // should throw if attempted to set anything other than an index on an + // array. + expect(() => create([]).set('x', true)).toThrow(); + // @ts-expect-error: testing number + expect(() => create([]).set(new Number(1), true)).toThrow(); // eslint-disable-line no-new-wrappers + expect(() => create({ x: { y: [] } }).set('x.y.z', true)).toThrow(); + + nota = new Notation({ x: { y: [1] } }); + expect(nota.set('x.y', 'value').value.x.y).toEqual('value'); + expect(nota.set('x.y', 'overwrite', false).value.x.y).toEqual('value'); + + // Cannot set value by inserting at index, on an object + expect(() => create().set('x', 1, 'insert')).toThrow(); + expect(() => create({ x: true }).set('x', 1, 'insert')).toThrow(); + }); + + test('#remove()', () => { + let nota = new Notation(structuredClone(o)); + const obj = nota.value; + // console.log('before', o); + expect(obj.age).toBeDefined(); + nota.remove('age'); + expect(obj.age).toBeUndefined(); + + expect(obj.company.address.city).toEqual('istanbul'); + nota.remove('company.address.city'); + expect(obj.company.address.city).toBeUndefined(); + // console.log('after', o); + + // deleting non-existing property.. + // this should have no effect. + const k = Object.keys(obj.company).length; + nota.remove('company.noProp'); + expect(Object.keys(obj.company).length).toEqual(k); + + const assets = { boat: 'none', car: { model: 'Mustang' } }; + create(assets).remove('car.model'); // alias + expect(assets.car).toEqual({}); + + // expect(() => create([{ x: 1 }]).remove('x')).toThrow(); // TODO: strict option + expect(create({ x: { y: 1 } }).remove('x').value).toEqual({}); + expect(create({ x: { y: 1 } }).remove('x.y').value).toEqual({ x: {} }); + expect(() => create({ x: { y: 1 } }).remove('x.*').value).toThrow(); + expect(create([{ x: 1 }]).remove('[0]').value).toEqual([]); + + nota = new Notation({ x: { y: [1], z: 5 } }, { strict: true }); + expect(nota.remove('x.z').value.z).toBeUndefined(); + expect(() => nota.remove('x.y[2]')).toThrow('Implied index'); + expect(() => nota.remove('x.z')).toThrow('Implied property'); + }); + + test('#clone()', () => { + expect(create(o).clone().value).toEqual(o); + + const obj: UnknownObject = { a: { b: { c: [1, 2, {}] } } }; + const clone = create(obj).clone().value; + expect(obj).toEqual(clone); + obj.x = true; + expect(clone.x).toBeUndefined(); + obj.a.b.c.push(3); + expect(clone.a.b.c.length).toEqual(3); + }); + + test('#merge(), #separate()', () => { + const nota = new Notation(structuredClone(o)); + nota.merge({ + key: null, + 'newkey.p1': 13, + 'newkey.p2': false, + 'newkey.p3.val': [] + }); + const merged = nota.value; + // console.log(JSON.stringify(merged, null, ' ')); + expect(merged.key).toEqual(null); + expect(merged.newkey.p1).toEqual(13); + expect(merged.newkey.p2).toEqual(false); + expect(Array.isArray(merged.newkey.p3.val)).toEqual(true); + + // @ts-expect-error: testing number + expect(() => create().merge(1)).toThrow(); + // below will fail bec. {} can be merged with array + expect(() => create().merge([])).toThrow(); // TODO: add more tests for merge() + expect(() => create([]).merge({ x: 1 })).toThrow(); // TODO: add more tests for merge() + // @ts-expect-error: testing null + expect(() => create().merge(null)).toThrow(); + + const separated = nota.separate(['newkey.p1', 'newkey.p2', 'newkey.p3.val']); + expect(separated.key).toBeUndefined(); + expect(separated.newkey.p1).toEqual(13); + expect(separated.newkey.p2).toEqual(false); + expect(Array.isArray(separated.newkey.p3.val)).toEqual(true); + + expect(merged.key).toEqual(null); + expect(merged.newkey.p1).toBeUndefined(); + expect(merged.newkey.p2).toBeUndefined(); + expect(merged.newkey.p3.val).toBeUndefined(); + + // @ts-expect-error: testing number + expect(() => create().separate(1)).toThrow(); + // @ts-expect-error: testing object + expect(() => create().separate({})).toThrow(); + // @ts-expect-error: testing null + expect(() => create().separate(null)).toThrow(); + }); + + test('#rename()', () => { + const nota = new Notation(structuredClone(o)); + const obj = nota.value; + expect(obj.company.address.location).toBeDefined(); + nota.rename('company.address.location', 'company.loc.geo'); + expect(obj.company.address.location).toBeUndefined(); + expect(typeof obj.company.loc.geo.lat === 'number').toEqual(true); + + expect(obj.name).toBeDefined(); + nota.rename('name', 'person'); + expect(obj.name).toBeUndefined(); + expect(obj.person).toBeDefined(); + nota.rename('person', 'me.name'); + expect(obj.person).toBeUndefined(); + expect(obj.me.name).toBeDefined(); + + expect(() => nota.rename('', 'test')).toThrow(); + expect(() => nota.rename('company', '')).toThrow(); + }); + + test('#copyToNew(), alias: #extract()', () => { + const nota = new Notation(structuredClone(o)); + const obj = nota.value; + // `extract(notation)` is same as `copyTo({}, notation)` + let ex; + ex = nota.extract('company'); + expect(obj.company.name).toEqual('pilot co'); + expect(ex.company.name).toEqual('pilot co'); + ex = nota.extract('company.address.country'); + expect(obj.company.address.country).toEqual('TR'); + expect(ex.company.address.country).toEqual('TR'); + }); + + test('#copyFrom()', () => { + const nota = new Notation(); + const src = { a: [{ x: 1 }], b: 2 }; + nota.copyFrom(src, 'a[0].x'); + expect(nota.value.a[0].x).toEqual(1); + nota.copyFrom(src, 'b', 'a[1]'); + expect(nota.value.a[1]).toEqual(2); + expect(nota.value.b).toBeUndefined(); + nota.copyFrom(src, 'none', null, true); + expect('none' in nota.value).toEqual(false); + + expect(src).toEqual({ a: [{ x: 1 }], b: 2 }); + + // @ts-expect-error: testing number + expect(() => nota.copyFrom(5, 'toString')).toThrow(); + }); + + test('#moveToNew(), alias: #extrude()', () => { + const nota = new Notation(structuredClone(o)); + const obj = nota.value; + // `extrude(notation)` is same as `moveTo({}, notation)` + let ex; + ex = nota.extrude('company.address.country'); + expect(obj.company.address.country).toBeUndefined(); + expect(ex.company.address.country).toEqual('TR'); + ex = nota.extrude('company', 'comp.my'); + expect(obj.company).toBeUndefined(); + expect(ex.company).toBeUndefined(); + expect(ex.comp.my.name).toEqual('pilot co'); + + // @ts-expect-error: testing number + expect(() => nota.moveTo(5, 'company')).toThrow(); + }); + + test('#moveFrom()', () => { + const nota = new Notation(); + const src = { a: [{ x: 1 }], b: 2 }; + nota.moveFrom(src, 'a[0].x'); + expect(nota.value.a[0].x).toEqual(1); + nota.moveFrom(src, 'b', 'a[1]'); + expect(nota.value.a[1]).toEqual(2); + expect(nota.value.b).toBeUndefined(); + nota.moveFrom(src, 'none', null, true); + expect('none' in nota.value).toEqual(false); + + expect(src).toEqual({ a: [{}] }); + + // @ts-expect-error: testing number + expect(() => nota.moveFrom(5, 'toString')).toThrow(); + }); + + test('return `undefined` for invalid notations', () => { + const nota = new Notation(structuredClone(o)); + const level1 = 'noProp'; + const level2 = 'noProp.level2'; + expect(nota.get(level1)).toBeUndefined(); + expect(nota.hasDefined(level1)).toEqual(false); + expect(nota.get(level2)).toBeUndefined(); + expect(nota.hasDefined(level2)).toEqual(false); + }); + + test('ignore invalid notations', () => { + const nota = new Notation(structuredClone(o)); + const obj = nota.value; + let ex; + const level1 = 'noProp'; + const level2 = 'noProp.level2'; + + ex = nota.extract(level1); + expect(nota.get(level1)).toBeUndefined(); + expect(Object.keys(ex).length).toEqual(0); + + ex = nota.extract(level2); + expect(nota.get(level2)).toBeUndefined(); + expect(Object.keys(ex).length).toEqual(0); + + ex = nota.extrude(level1); + expect(nota.get(level1)).toBeUndefined(); + expect(Object.keys(ex).length).toEqual(0); + + ex = nota.extrude(level2); + expect(nota.get(level2)).toBeUndefined(); + expect(Object.keys(ex).length).toEqual(0); + + expect(obj.account.noProp).toBeUndefined(); + nota.rename('account.noProp', 'renamedProp'); + expect(obj.renamedProp).toBeUndefined(); + }); + + test('throw if invalid object or notation', () => { + // @ts-expect-error: testing null + expect(() => new Notation(null)).toThrow(); + expect(() => new Notation(undefined)).toThrow(); + expect(() => new Notation(new Date())).toThrow(); + expect(() => new Notation(Number)).toThrow(); + // @ts-expect-error: testing number + expect(() => create(1)).toThrow(); + // @ts-expect-error: testing boolean + expect(() => create(true)).toThrow(); + // @ts-expect-error: testing string + expect(() => create('no')).toThrow(); + + const nota = new Notation(structuredClone(o)); + // @ts-expect-error: testing null + expect(() => nota.copyTo(null, 'account')).toThrow(); + // @ts-expect-error: testing number + expect(() => nota.has({}, 4)).toThrow(); + // @ts-expect-error: testing object + expect(() => nota.remove({})).toThrow(); + // @ts-expect-error: testing object + expect(() => nota.rename('account', {})).toThrow(); + expect(() => nota.get('prop2')).not.toThrow(); + }); + + test('property names', () => { + const a = { + x: 1, + y: { c: 2 }, + '@test': true + }; + const notation = new Notation(a); + expect(notation.set('y.c', 5).value.y.c).toEqual(5); + expect(notation.set('["@test"]', false).value['@test']).toEqual(false); + }); +}); diff --git a/test/NotationError.spec.ts b/test/NotationError.spec.ts new file mode 100644 index 0000000..78b87f3 --- /dev/null +++ b/test/NotationError.spec.ts @@ -0,0 +1,42 @@ +import { NotationError } from '../src/core/NotationError.js'; + +describe('NotationError', () => { + test('is an Error / NotationError with the right name and message', () => { + const err = new NotationError('boom'); + expect(err).toBeInstanceOf(Error); + expect(err).toBeInstanceOf(NotationError); + expect(err.name).toEqual('NotationError'); + expect(err.message).toEqual('boom'); + expect(String(err)).toEqual('NotationError: boom'); + }); + + test('defaults the message to an empty string', () => { + expect(new NotationError().message).toEqual(''); + }); + + test('`name` is non-enumerable and non-writable', () => { + const err = new NotationError('x'); + const d = Object.getOwnPropertyDescriptor(err, 'name'); + expect(d?.enumerable).toEqual(false); + expect(d?.writable).toEqual(false); + expect(Object.keys(err)).not.toContain('name'); + }); + + test('`message` is non-enumerable but writable', () => { + const err = new NotationError('x'); + const d = Object.getOwnPropertyDescriptor(err, 'message'); + expect(d?.enumerable).toEqual(false); + expect(d?.writable).toEqual(true); + expect(Object.keys(err)).not.toContain('message'); + }); + + test('has a stack trace', () => { + expect(typeof new NotationError('x').stack).toEqual('string'); + }); + + test('is catchable as a thrown error', () => { + expect(() => { + throw new NotationError('nope'); + }).toThrow(/nope/); + }); +}); diff --git a/test/NotationGlob.spec.ts b/test/NotationGlob.spec.ts new file mode 100644 index 0000000..8092083 --- /dev/null +++ b/test/NotationGlob.spec.ts @@ -0,0 +1,1068 @@ +/* eslint camelcase:0, max-lines-per-function:0, consistent-return:0, max-statements:0, max-lines:0, max-len:0 */ + +import { NotationGlob } from '../src/core/NotationGlob.js'; +import type { UnknownObject } from '../src/types.js'; + +// shuffle array +function shuffle(o: string[]): string[] { + // v1.0 + for ( + let j, x, i = o.length; + i; + j = Math.floor(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x + ); + return o; +} + +describe('NotationGlob', () => { + const { isValid, hasMagic, toRegExp, split, compare, sort, normalize, union, create } = + NotationGlob; + + // private methods + const _inspect = (NotationGlob as UnknownObject)._inspect; + const _covers = (NotationGlob as UnknownObject)._covers; + const _intersect = (NotationGlob as UnknownObject)._intersect; + + const reVAR = '[a-z$_][a-z$_\\d]*'; + const reARRINDEX = '\\[\\d+\\]'; + const reREST = '(?:[\\[\\.].+|$)'; + + test('.isValid()', () => { + expect(isValid('prop.mid.last')).toEqual(true); + expect(isValid('prop.*.')).toEqual(false); + expect(isValid('prop.*')).toEqual(true); + expect(isValid('prop.')).toEqual(false); + expect(isValid('prop')).toEqual(true); + expect(isValid('pro*')).toEqual(false); + expect(isValid('.prop')).toEqual(false); + expect(isValid('.')).toEqual(false); + // @ts-expect-error: testing no args + expect(isValid()).toEqual(false); + // @ts-expect-error: testing number + expect(isValid(1)).toEqual(false); + // @ts-expect-error: testing null + expect(isValid(null)).toEqual(false); + // @ts-expect-error: testing boolean + expect(isValid(true)).toEqual(false); + expect(isValid('')).toEqual(false); + expect(isValid('*.')).toEqual(false); + expect(isValid('*')).toEqual(true); + expect(isValid('.*')).toEqual(false); + expect(isValid('***')).toEqual(false); + expect(isValid('!*')).toEqual(true); + expect(isValid('!**')).toEqual(false); + expect(isValid('!*.*')).toEqual(true); + expect(isValid('!*.**')).toEqual(false); + expect(isValid('!*.*.xxx')).toEqual(true); + + expect(isValid('')).toEqual(false); + expect(isValid('!')).toEqual(false); + expect(isValid('*')).toEqual(true); + expect(isValid('[*]')).toEqual(true); + expect(isValid('["*"]')).toEqual(true); // not wildcard but valid + expect(isValid('["[*]"]')).toEqual(true); // not wildcard but valid + expect(isValid('[]')).toEqual(false); + expect(isValid('[!]')).toEqual(false); + expect(isValid('a[0][1][2].*')).toEqual(true); + expect(isValid('a.b[*].x')).toEqual(true); + expect(isValid('a.b[3]')).toEqual(true); + expect(isValid('a.*[*]')).toEqual(true); + expect(isValid('a.*[*].*')).toEqual(true); + expect(isValid('a.*[1]')).toEqual(true); + expect(isValid('a.*.c[1]')).toEqual(true); + expect(isValid('!a.*.c[1]')).toEqual(true); + expect(isValid('a.*[*][0].c')).toEqual(true); + expect(isValid('!*.*[*][*].*')).toEqual(true); + expect(isValid('x.*["*"][*][1].*["*.x"]')).toEqual(true); + expect(isValid('*.[*]')).toEqual(false); + expect(isValid('*[*]*[*]')).toEqual(false); + expect(isValid('*[*]*.[*]')).toEqual(false); + }); + + test('.hasMagic()', () => { + expect(hasMagic('x.y.z')).toEqual(false); + expect(hasMagic('!x.y.z')).toEqual(true); + expect(hasMagic('x.y.!z')).toEqual(false); // invalid + expect(hasMagic('[*]')).toEqual(true); + expect(hasMagic('x.*')).toEqual(true); + expect(hasMagic('[*].x.y.*')).toEqual(true); + expect(hasMagic('')).toEqual(false); + // @ts-expect-error: testing null + expect(hasMagic(null)).toEqual(false); // invalid + // @ts-expect-error: testing boolea + expect(hasMagic(true)).toEqual(false); // invalid + expect(hasMagic('*.')).toEqual(false); // invalid + }); + + test('.toRegExp()', () => { + // @ts-expect-error: testing no args + expect(() => toRegExp().source).toThrow(); + expect(() => toRegExp('').source).toThrow(); + expect(() => toRegExp('*.').source).toThrow(); + expect(toRegExp('*').source).toEqual('^' + reVAR + reREST); + expect(toRegExp('[*]').source).toEqual('^' + reARRINDEX + reREST); + expect(toRegExp('["*"]').source).toEqual('^\\["\\*"\\]' + reREST); + expect(toRegExp('x.*').source).toEqual('^x\\.' + reVAR + reREST); + expect(toRegExp('!x.*["x.*"][1]').source).toEqual( + '^x\\.' + reVAR + '\\["x\\.\\*"\\]\\[1\\]' + reREST + ); + }); + + test('._inspect()', () => { + let ins = _inspect('![*].x.y[1].*[*]'); + expect(ins.glob).toEqual('![*].x.y[1].*[*]'); + expect(ins.absGlob).toEqual('[*].x.y[1].*[*]'); // 'cause original is negated + expect(ins.isNegated).toEqual(true); + + ins = _inspect('*.x[*].y'); + expect(ins.glob).toEqual('*.x[*].y'); + expect(ins.absGlob).toEqual('*.x[*].y'); + expect(ins.isNegated).toEqual(false); + }); + + test('.split()', () => { + // @ts-expect-error: testing no args + expect(() => split()).toThrow(); + expect(() => split('')).toThrow(); + expect(() => split('.x')).toThrow(); + expect(() => split('[x]')).toThrow(); + expect(split('x')).toEqual(['x']); + expect(split('*')).toEqual(['*']); + expect(split('[*]')).toEqual(['[*]']); + expect(split('["[*]"]')).toEqual(['["[*]"]']); + expect(split('!["ab.c"]')).toEqual(['["ab.c"]']); + expect(split('*.x.y[*][1].z["*.x"].a')).toEqual([ + '*', + 'x', + 'y', + '[*]', + '[1]', + 'z', + '["*.x"]', + 'a' + ]); + expect(split('*.*[*]')).toEqual(['*', '*', '[*]']); + // normalized/cleaned + expect(split('*.*[*]', true)).toEqual(['*']); + expect(split('[*].*[*].*', true)).toEqual(['[*]']); + expect(split('x.y[*]', true)).toEqual(['x', 'y']); + expect(split('!x.y[*]', true)).toEqual(['x', 'y', '[*]']); + expect(split('x.y.*.z', true)).toEqual(['x', 'y', '*', 'z']); + expect(split('x.y[*].z.*', true)).toEqual(['x', 'y', '[*]', 'z']); + }); + + test('.compare()', () => { + expect(compare('*', 'a')).toEqual(-1); + expect(compare('a', '*')).toEqual(1); + expect(compare('!*', 'a')).toEqual(-1); + expect(compare('*', '*')).toEqual(0); + expect(compare('*', '[*]')).toEqual(0); + expect(compare('[*]', '*')).toEqual(0); + expect(compare('[*]', '[*]')).toEqual(0); + expect(compare('[*]', 'a')).toEqual(-1); + expect(compare('a', '[*]')).toEqual(1); + expect(compare('*.*', '*')).toEqual(0); + expect(compare('*.*.*', '*')).toEqual(0); + expect(compare('*', '*[*].*')).toEqual(0); + expect(compare('*.*', '*.a')).toEqual(-1); + expect(compare('*', '!*.*')).toEqual(-1); + expect(compare('*.x', 'y.*')).toEqual(1); + expect(compare('[*].x', 'a.*')).toEqual(1); + expect(compare('*.x.*', 'x.*')).toEqual(1); + expect(compare('*.x.y', 'x.y')).toEqual(1); + + expect(compare('a.*', 'a.b[1]')).toEqual(-1); + expect(compare('a.b', 'a[*]')).toEqual(1); + expect(compare('a.b[3]', 'a.b[2]')).toEqual(1); + expect(compare('a.*[*]', 'a.*')).toEqual(0); + expect(compare('a', 'a[*]')).toEqual(0); + expect(compare('a.*[1]', 'a.*[*]')).toEqual(1); + expect(compare('a.*.c[1]', 'a.*.c[*]')).toEqual(1); + expect(compare('a.*.c[1]', 'a.*.c')).toEqual(1); + expect(compare('a.*.c', 'a.*.c[*]')).toEqual(0); + + expect(compare('[0]', '[0]')).toEqual(0); + expect(compare('[1][0]', '[2][0]')).toEqual(-1); + expect(compare('[1]', '[0]')).toEqual(1); + expect(compare('![1]', '[0]')).toEqual(1); + // higher (last item) index should come first both if negeated. ![*] + // should come last. + expect(compare('![*]', '![0]')).toEqual(1); + expect(compare('![1]', '![0]')).toEqual(-1); + expect(compare('![1][*]', '![1][1]')).toEqual(1); + expect(compare('![*][1]', '![0][2]')).toEqual(1); + expect(compare('![*][1]', '![1][1]')).toEqual(-1); + // not same array items, regular sorting + expect(compare('![1][0]', '![2][0]')).toEqual(-1); + }); + + test('.sort()', () => { + const globs = [ + '!prop.name', + 'bill.account.credit', + 'bill.account.*', + '!*.account.*.name', + 'account.tags', + '!account.id', + 'bill.*.*', + 'prop.id', + 'account.likes[*]', + '*.account.id', + '*', + '!*.account.*', + '*.*.credit', + 'account.*', + 'account.id', + 'prop.x[2].y', + '!x[1].foo', + 'prop.*', + '!prop.*.name', + 'x[*].foo', + '!foo.*.boo', + 'foo.qux.*' + ]; + + const expectedSorted = [ + '*', + 'account.*', + 'bill.*.*', + 'prop.*', + 'account.id', + 'account.likes[*]', + 'account.tags', + 'bill.account.*', + 'foo.qux.*', + 'prop.id', + '!account.id', + '!prop.name', + '*.*.credit', + '*.account.id', + 'x[*].foo', + '!*.account.*', + '!foo.*.boo', + '!prop.*.name', + 'bill.account.credit', + '!x[1].foo', + '!*.account.*.name', + 'prop.x[2].y' + ]; + + // '!foo.*.boo' vs 'foo.qux.*' => '!foo.*.boo', 'foo.qux.*' + // 'foo.*.boo' vs '!foo.qux.*' => 'foo.*.boo', '!foo.qux.*' + + let i; + let shuffled: string[]; + + const indexOf = (v: string): number => shuffled.indexOf(v); + + for (i = 0; i <= 10; i++) { + shuffled = shuffle(globs.concat()); + shuffled = sort(shuffled); + // console.log(shuffled); + expect(shuffled).toEqual(expectedSorted); + expect(indexOf('*')).toEqual(0); + expect(indexOf('account.*')).toBeLessThan(indexOf('account.tags')); + expect(indexOf('account.*')).toBeLessThan(indexOf('account.id')); + expect(indexOf('account.*')).toBeLessThan(indexOf('!account.id')); + expect(indexOf('account.id')).toBeLessThan(indexOf('!account.id')); + expect(indexOf('bill.*.*')).toBeLessThan(indexOf('bill.account.credit')); + expect(indexOf('bill.account.*')).toBeLessThan(indexOf('bill.account.credit')); + expect(indexOf('bill.*.*')).toBeLessThan(indexOf('bill.account.*')); + expect(indexOf('*.account.*')).toBeLessThan(indexOf('*.account.id')); + expect(indexOf('*.account.id')).toBeLessThan(indexOf('!*.account.*')); + expect(indexOf('*.account.id')).toBeLessThan(indexOf('!*.account.*.name')); + expect(indexOf('*.*.credit')).toBeLessThan(indexOf('!*.account.*.name')); + expect(indexOf('*.*.credit')).toBeLessThan(indexOf('bill.account.credit')); + expect(indexOf('prop.*')).toBeLessThan(indexOf('prop.id')); + expect(indexOf('prop.*')).toBeLessThan(indexOf('!prop.*.name')); + expect(indexOf('x[*].foo')).toBeLessThan(indexOf('!x[1].foo')); + expect(indexOf('prop.*')).toBeLessThan(indexOf('prop.x[2].y')); + } + }); + + test('.sort() » negated comes last', () => { + const original = [ + 'foo.bar.baz', + 'bar.name', + '!foo.*.baz', + '!bar.*', + '!foo.qux.boo', + 'foo.qux.*', + 'bar.id', + '!bar.id' + ]; + + let i; + let shuffled; + let sorted; + let indexN; + let indexNeg; + + for (i = 0; i <= 10; i++) { + shuffled = shuffle(original.concat()); + // console.log(shuffled); + sorted = sort(shuffled); + indexN = sorted.indexOf('bar.id'); + indexNeg = sorted.indexOf('!bar.id'); + + expect(indexNeg).toBeGreaterThan(-1); + expect(indexN).toBeGreaterThan(-1); + expect(indexNeg).toBeGreaterThan(indexN); + } + }); + + test('constructor, .create()', () => { + const g1 = new NotationGlob('!*.x[1].*'); + expect(g1 instanceof NotationGlob).toEqual(true); + expect(g1.glob).toEqual('!*.x[1].*'); + expect(g1.absGlob).toEqual('*.x[1].*'); + expect(g1.isNegated).toEqual(true); + expect(g1.regexp.source).toEqual('^' + reVAR + '\\.x\\[1\\]\\.' + reVAR + reREST); + expect(g1.regexp.flags).toEqual('i'); + expect(g1.notes).toEqual(['*', 'x', '[1]', '*']); + expect(g1.test('y')).toEqual(false); + expect(g1.test('y.x[1]')).toEqual(false); + expect(g1.test('y.x[1].z')).toEqual(true); + expect(g1.test('y.z')).toEqual(false); + + const g2 = create('[*].x'); + expect(g2 instanceof NotationGlob).toEqual(true); + expect(g2.glob).toEqual('[*].x'); + expect(g2.absGlob).toEqual('[*].x'); + expect(g2.isNegated).toEqual(false); + expect(g2.regexp.source).toEqual('^' + reARRINDEX + '\\.x' + reREST); + expect(g2.regexp.flags).toEqual('i'); + expect(g2.notes).toEqual(['[*]', 'x']); + expect(g2.test('[0]')).toEqual(false); + expect(g2.test('[1].x')).toEqual(true); + expect(g2.test('[1].y')).toEqual(false); + + expect(() => g2.test('*')).toThrow(); + expect(() => g2.test('[1].*')).toThrow(); + + // @ts-expect-error: testing no args + expect(() => new NotationGlob()).toThrow(); + expect(() => new NotationGlob('')).toThrow(); + expect(() => new NotationGlob('%.s')).toThrow(); + // @ts-expect-error: testing no args + expect(() => create()).toThrow(); + expect(() => create('')).toThrow(); + expect(() => create('s .')).toThrow(); + }); + + test('#parent, #last, #first', () => { + expect(create('*').parent).toEqual(null); + expect(create('*.x').parent).toEqual('*'); + expect(create('*.x.*').parent).toEqual('*'); + expect(create('*.x[*]').parent).toEqual('*'); + expect(create('[*].x').parent).toEqual('[*]'); + expect(create('x.y.z').parent).toEqual('x.y'); + + expect(create('*').last).toEqual('*'); + expect(create('*.x').last).toEqual('x'); + expect(create('*.x.*').last).toEqual('x'); + expect(create('*.x[*]').last).toEqual('x'); + expect(create('[*].x').last).toEqual('x'); + expect(create('x.y.z').last).toEqual('z'); + + expect(create('*').first).toEqual('*'); + expect(create('*.x').first).toEqual('*'); + expect(create('*.x.*').first).toEqual('*'); + expect(create('*.x[*]').first).toEqual('*'); + expect(create('[*].x').first).toEqual('[*]'); + expect(create('x.y.z').first).toEqual('x'); + }); + + test('#test()', () => { + let strNota = 'account.id'; + expect(create('account.id').test(strNota)).toEqual(true); + expect(create('account.*').test(strNota)).toEqual(true); + expect(create('*.*').test(strNota)).toEqual(true); + expect(create('*').test(strNota)).toEqual(true); + expect(create('billing.account.id').test(strNota)).toEqual(false); + expect(create(strNota).test('billing.account.id')).toEqual(false); + + strNota = 'list[1].id'; + expect(create('list[1].id').test(strNota)).toEqual(true); + expect(create('list[2].id').test(strNota)).toEqual(false); + expect(create('list[*]').test(strNota)).toEqual(true); + expect(create('list[*].*').test(strNota)).toEqual(true); + expect(create('*').test(strNota)).toEqual(true); + expect(create('[*]').test(strNota)).toEqual(false); + expect(create('x.list[1].id').test(strNota)).toEqual(false); + expect(create(strNota).test('x.list[2].id')).toEqual(false); + + strNota = '[1].id'; + expect(create('[1].id').test(strNota)).toEqual(true); + expect(create('[2].id').test(strNota)).toEqual(false); + expect(create('[*]').test(strNota)).toEqual(true); + expect(create('[*].*').test(strNota)).toEqual(true); + expect(create('*').test(strNota)).toEqual(false); + expect(create('[*]').test(strNota)).toEqual(true); + expect(create('x[1].id').test(strNota)).toEqual(false); + expect(create(strNota).test('x[1].id')).toEqual(false); + }); + + test('#covers(), ._covers()', () => { + const cov = (globA: string, globB: string | string[] | NotationGlob): boolean => + create(globA).covers(globB); + + expect(cov('*.*', 'b')).toEqual(true); + expect(cov('*.b', 'b')).toEqual(false); + expect(cov('a.*', 'b')).toEqual(false); + expect(cov('a.*', 'a')).toEqual(true); + expect(cov('*', 'b')).toEqual(true); + expect(cov('a', 'b')).toEqual(false); + expect(cov('a.b.c', 'a.b.c')).toEqual(true); + expect(cov('a.b', 'a.b.c')).toEqual(true); + expect(cov('a.b.c', 'a.b')).toEqual(false); + expect(cov('*', 'b.*')).toEqual(true); + expect(cov('*', 'b.*')).toEqual(true); + expect(cov('b.*', '*')).toEqual(false); + expect(cov('a', '*')).toEqual(false); + expect(cov('a', 'b.*')).toEqual(false); + expect(cov('a', 'a')).toEqual(true); + expect(cov('a', '!a')).toEqual(true); + expect(cov('!a', 'a')).toEqual(true); + expect(cov('a.*', 'a.b')).toEqual(true); + expect(cov('a.*', 'a.b[1]')).toEqual(true); + expect(cov('a.*', 'a.b[*].c')).toEqual(true); + expect(cov('a.b[*].c', 'a.b[*]')).toEqual(false); + expect(cov('a.b[*]', new NotationGlob('a.b[2].c'))).toEqual(true); + expect(cov('[1].*.b[*].*.d', '[1].a.b[3].c.d')).toEqual(true); + expect(cov('[1].*.b[*].*.d', '[2].a.b')).toEqual(false); + + // static private method: _covers() + expect(_covers('*', 'b')).toEqual(true); + expect(_covers('a', 'b')).toEqual(false); + expect(_covers('a.b.c', 'a.b.c')).toEqual(true); + expect(_covers('*', '*.b')).toEqual(true); + expect(_covers('a.b[*].c', 'a.b[*]')).toEqual(false); + expect(_covers('a.b[*]', new NotationGlob('a.b[2].c'))).toEqual(true); + expect(_covers('!*', 'b')).toEqual(true); + expect(_covers('b', '!*')).toEqual(false); + expect(_covers('!*.b', 'a.b')).toEqual(true); + expect(_covers('a.b', '!*.b')).toEqual(false); + expect(_covers('!*.b', 'y.b.c')).toEqual(true); + expect(_covers('![*].b', '[2].b.c')).toEqual(true); + expect(_covers('!*.b', '["2"].b.c')).toEqual(true); + + // check for 'match' instead of 'covers' (3rd arg set to `true`) + expect(_covers('[*]', '[0]', true)).toEqual(true); + expect(_covers('[0]', '[*]', true)).toEqual(true); + expect(_covers('[*]', '[1]', true)).toEqual(true); + expect(_covers('[1]', '[*]', true)).toEqual(true); + expect(_covers('[0][*]', '[*][1]', true)).toEqual(true); + expect(_covers('[*][1]', '[0][*]', true)).toEqual(true); + expect(_covers('[*][*]', '[*][*]', true)).toEqual(true); + expect(_covers('[4][*]', '[*][*][1]', true)).toEqual(true); + + // object key bracket notation + expect(_covers("*['x']", 'a.x')).toEqual(true); + expect(_covers('*["x"]', 'a.x')).toEqual(true); + expect(_covers('*["x"]', 'a["x"]')).toEqual(true); + expect(_covers("*['x']", "a['x']")).toEqual(true); + expect(_covers('*', "['x']")).toEqual(true); + expect(_covers('*', '["x.y"]')).toEqual(true); + expect(_covers('[*]', "['x']")).toEqual(false); + expect(_covers('*', "['*']")).toEqual(true); + expect(_covers('*', "['[*]']")).toEqual(true); + expect(_covers('[*]', "['[*]']")).toEqual(false); + expect(_covers('x.y', "['x.y']")).toEqual(false); + expect(_covers('x.y', "x['y']")).toEqual(true); + expect(_covers('x.y', "x['y']")).toEqual(true); + }); + + test('#intersect(), ._intersect()', () => { + const intersect = (globA: string, globB: string, restrictive: boolean = false): string | null => + create(globA).intersect(globB, restrictive); + + // x.* ∩ *.y » x.y + // x.*.* ∩ *.y » x.y.* + // x.*.z ∩ *.y » x.y.z + // x.y ∩ *.b » (n/a) + // x.y ∩ a.* » (n/a) + expect(_intersect('x.*', '*.z')).toEqual('x.z'); + expect(_intersect('x.*', '*.z', true)).toEqual('x.z'); + expect(_intersect('x.*.*', '*.y')).toEqual('x.y'); + expect(_intersect('x.*.z', '!*.y')).toEqual('x.y.z'); // asuming y is object + expect(_intersect('!x.*.z', '*.y')).toEqual('!x.y.z'); // asuming y is object + expect(_intersect('x.*.z', '!*.y', true)).toEqual('!x.y.z'); // asuming y is object + expect(_intersect('x.y', '*.b')).toEqual(null); + expect(_intersect('x.y', 'a.*')).toEqual(null); + expect(_intersect('x.*.*.z.*', 'x.a.*.z.b')).toEqual('x.a.*.z.b'); + expect(_intersect('!x.*.*.z.x', 'x.*.*.z.*')).toEqual('!x.*.*.z.x'); + expect(_intersect('!x.*.*.z.x', 'x.*.*.z.*', true)).toEqual('!x.*.*.z.x'); + expect(_intersect('x.*.*.z.x', 'x.*.*.z.y')).toEqual(null); + expect(_intersect('x.a.*.z.x', 'x.b.*.z.y')).toEqual(null); + expect(_intersect('x.*.*.z.*', 'x.*')).toEqual('x.*.*.z'); + expect(_intersect('x.*.*', 'x.o')).toEqual('x.o'); + expect(_intersect('x.*[*]', 'x.o')).toEqual('x.o'); + expect(_intersect('!x.*.*', '!x.o')).toEqual('!x.o.*'); + expect(_intersect('a.*', '!*.z')).toEqual('!a.z'); + expect(_intersect('*.z', '!a.*')).toEqual('a.z'); + expect(_intersect('*.z', '!a.*', true)).toEqual('!a.z'); + expect(_intersect('*.z', 'a.*')).toEqual('a.z'); + + // instance method #intersect() also normalizes the glob + expect(intersect('x.*.*', '*.y')).toEqual('x.y'); + expect(intersect('x.*.z', '!*.y')).toEqual('x.y.z'); + expect(intersect('x.*.z', '!*.y', true)).toEqual('!x.y.z'); + expect(intersect('x.y', '*.b')).toEqual(null); + expect(intersect('x.*[*]', 'x.o')).toEqual('x.o'); + expect(intersect('!x.*.*', '!x.o')).toEqual('!x.o.*'); + expect(intersect('a.*', '!*.z')).toEqual('!a.z'); + expect(intersect('*.z', '!a.*')).toEqual('a.z'); + expect(intersect('*.z', '!a.*', true)).toEqual('!a.z'); + expect(intersect('*.z', 'a.*')).toEqual('a.z'); + + // check default value for restrictive argument + expect(create('*.z').intersect('!a.*')).toEqual('a.z'); + expect(create('*.z').intersect('!a.*', true)).toEqual('!a.z'); + }); + + test('.normalize() restrictive = false', () => { + const norm = (globs: string | string[]): string[] => normalize(globs, false); + + expect(() => norm(['*.[*]'])).toThrow(); + expect(() => norm(['*.[*]', 'x'])).toThrow(); + expect(() => norm(['*[*]*[*]'])).toThrow(); + expect(() => norm(['*[*'])).toThrow(); + expect(() => norm(['*', 'x-1'])).toThrow(); + + expect(norm(['*'])).toEqual(['*']); + expect(norm(['*.*'])).toEqual(['*']); + expect(norm(['*.*.*'])).toEqual(['*']); + expect(norm(['*[*].*'])).toEqual(['*']); + expect(norm(['*[*].*[*]'])).toEqual(['*']); + expect(norm(['*', '*'])).toEqual(['*']); + expect(norm(['x', 'x'])).toEqual(['x']); + expect(norm(['[*]'])).toEqual(['[*]']); + expect(norm(['!*'])).toEqual([]); + expect(norm(['![*]'])).toEqual([]); + expect(norm(['*', '!*'])).toEqual([]); + expect(norm(['!*', '*'])).toEqual([]); + + // higher arr index item globs should come first, if negated bec. they + // should be removed first to prevent shifted indexes. + expect(norm(['[*]', '![*][1]', '![0][1]', '![0][2]', '![1][2][0]'])).toEqual([ + '[*]', + '![0][2]', + '![*][1]', + '![1][2][0]' + ]); + expect( + norm(['![1][0][3]', '[*]', '![1][*][5]', '![*][0][4]', '![0][3]', '![1][2][3]']) + ).toEqual(['[*]', '![0][3]', '![1][*][5]', '![*][0][4]', '![1][0][3]', '![1][2][3]']); + + expect(norm(['*', 'user', 'video'])).toEqual(['*']); + expect(norm(['!*', 'user', 'video'])).toEqual(['user', 'video']); + expect(norm(['user', 'video', '!*'])).toEqual(['user', 'video']); // order shouldn't matter + expect(norm(['!*', '!user', 'video'])).toEqual(['video']); + + expect(norm(['user.*', '!user.id'])).toEqual(['user', '!user.id']); + expect(norm(['!user.*', 'user.id'])).toEqual(['user.id']); // more explicit wins + expect(norm(['!*.id', 'user.id'])).toEqual(['user.id']); // more explicit wins + + expect(norm(['*', '!*.id', 'user.id'])).toEqual(['*', '!*.id', 'user.id']); // explicit remains + expect(norm(['user', '!*.id', 'user.id'])).toEqual(['user']); // explicit remains + + expect(norm(['!user.id', 'user.id'])).toEqual([]); // exact negated wins + expect(norm(['*', '!user.*', 'user.id'])).toEqual(['*', '!user.*', 'user.id']); + expect(norm(['user', '!*.id'])).toEqual(['user', '!user.id']); + expect(norm(['*', 'user', '!*.id'])).toEqual(['*', '!*.id']); + + expect(norm(['user', '!*.id', 'car.id'])).toEqual(['user', 'car.id', '!user.id']); // explicit remains + expect(norm(['user', '!*.id', 'car.id', 'user.id'])).toEqual(['user', 'car.id']); // explicit remains + + expect(norm(['!user.*.*', 'user.profile'])).toEqual(['user.profile', '!user.profile.*']); + + expect(norm(['user', 'user.id', '!*.id', 'video.id'])).toEqual(['user', 'video.id']); + expect(norm(['*', 'user', 'user.id', '!*.id', 'video.id'])).toEqual([ + '*', + '!*.id', + 'user.id', + 'video.id' + ]); + expect(norm(['video.id', '!*.id', 'user', 'user.id', '*'])).toEqual([ + '*', + '!*.id', + 'user.id', + 'video.id' + ]); // re-ordered version of above + expect(norm(['*', 'user', '!*.id', 'video.id'])).toEqual(['*', '!*.id', 'video.id']); + + expect(norm(['*', 'user.id', '!*.id', 'video.id'])).toEqual([ + '*', + '!*.id', + 'user.id', + 'video.id' + ]); + + expect(norm(['*', '!user.pwd'])).toEqual(['*', '!user.pwd']); + expect(norm(['*', 'user.*', '!user.pwd'])).toEqual(['*', '!user.pwd']); + + expect( + norm(['*', '!id', 'name', 'car.model', '!car.*', 'id', 'name', 'user', '!user.pwd']) + ).toEqual(['*', '!id', '!car.*', 'car.model', '!user.pwd']); + + expect(norm(['*', '!id', 'car.model', '!car.*', '!user.pwd'])).toEqual([ + '*', + '!id', + '!car.*', + 'car.model', + '!user.pwd' + ]); + + expect(norm(['*', 'car.model', '!car'])).toEqual(['*', '!car', 'car.model']); + + expect(norm(['name', 'pwd', '!id'])).toEqual(['name', 'pwd']); + + expect(norm(['!x.o.y', 'x.o'])).toEqual(['x.o', '!x.o.y']); + expect(norm(['!x.o.*', 'x.o'])).toEqual(['x.o', '!x.o.*']); + expect(norm(['!x.*.*', 'x.o'])).toEqual(['x.o', '!x.o.*']); + expect(norm(['!x.*.*', '*', 'x.o', 'id'])).toEqual(['*', '!x.*.*']); + + expect(norm(['!a.*', 'a'])).toEqual(['a', '!a.*']); // Notation#filter() would return {} + // should be treated same as above + expect(norm(['!a[*]', 'a'])).toEqual(['a', '!a[*]']); // Notation#filter() would return [] + expect(norm(['*', 'a[*]', '!a[*]'])).toEqual(['*', '!a[*]']); + expect(norm(['a[*]', '!a[*]'])).toEqual(['a', '!a[*]']); + expect(norm(['a[*]', '!a'])).toEqual([]); + expect(norm(['!a', 'a[*]'])).toEqual([]); + expect(norm(['!a[*]', 'a[*]'])).toEqual(['a', '!a[*]']); // results in empty array + expect(norm(['!a[*]', 'a[1]'])).toEqual(['a[1]']); + expect(norm(['*', '!a[*]', 'a[1]'])).toEqual(['*', '!a[*]', 'a[1]']); + expect(norm(['a[*]', '!a[0][1][2]'])).toEqual(['a', '!a[0][1][2]']); + expect(norm(['a[4]', 'a[*]'])).toEqual(['a']); + expect(norm(['a.*', 'a.*[*]'])).toEqual(['a']); + expect(norm(['a.*', 'a.*[2]'])).toEqual(['a']); + expect(norm(['a.b', 'a.*[*]', 'a.c[2].*'])).toEqual(['a']); + + expect(norm(['!x', 'c[1]', '!c[*]', '*', '!d.e'])).toEqual([ + '*', + '!x', + '!c[*]', + 'c[1]', + '!d.e' + ]); + + expect(norm(['!id', 'name', 'car.model', '!car.*', 'id', '!email'])).toEqual([ + 'name', + 'car.model' + ]); + + expect(norm(['!y.*', 'x.x[1][0][*]', '*.x[*]', '!x.x[2][*]', 'a.b', 'c[*][1]'])).toEqual([ + '*.x', + 'a.b', + 'c[*][1]', + '!x.x[2][*]' + ]); + + expect( + norm([ + 'bar.name', + '!bar.id', + 'bar', + 'foo.bar.baz', + 'foo.qux', + '!bar.id', + 'bar.id', + '!foo.*.baz' + ]) + ).toEqual(['bar', 'foo.qux', '!bar.id', 'foo.bar.baz', '!foo.qux.baz']); + + expect(norm(['!*', 'a'])).toEqual(['a']); + expect(norm(['!*.b', 'a.b'])).toEqual(['a.b']); + expect(norm(['!a', 'a.b', 'x[*].*'])).toEqual(['x', 'a.b']); + expect(norm(['a.*', '!*.z'])).toEqual(['a', '!a.z']); + expect(norm(['a', '!a.z'])).toEqual(['a', '!a.z']); + expect(norm(['x.*', '!*.y'])).toEqual(['x', '!x.y']); + + expect(norm(['!a.x', 'a.x', '!a.*', '!*'])).toEqual([]); + + expect(norm(['!a.*', '*.x'])).toEqual(['*.x']); + expect(norm(['!a.*', '*.x', '*.y'])).toEqual(['*.x', '*.y']); + expect(norm(['a', 'b.c', '!x', '*.y'])).toEqual(['a', '*.y', 'b.c']); + expect(norm(['a', 'b.c', '!x', '!*.y'])).toEqual(['a', 'b.c', '!a.y']); + expect(norm(['*', 'a', '!x', '!*.y'])).toEqual(['*', '!x', '!*.y']); + expect(norm(['!a.b.*', 'a.b.c'])).toEqual(['a.b.c']); + expect(norm(['!a.*', '!a.b.*', 'a.b.c'])).toEqual(['a.b.c']); + + expect(norm(['!x.*', 'x.y'])).toEqual(['x.y']); + expect(norm(['x.*', '!*.y'])).toEqual(['x', '!x.y']); + expect(norm(['a', 'x', '!a.z', '!x.y'])).toEqual(['a', 'x', '!a.z', '!x.y']); + expect(norm(['*', '!*.b', 'x.b', 'a'])).toEqual(['*', '!*.b', 'x.b']); + expect(norm(['*.x', '!*.*.b', 'x.a.b'])).toEqual(['*.x', '!*.x.b', 'x.a.b']); + expect(norm(['*', '*.x', '!*.*.b', 'x.a.b'])).toEqual(['*', '!*.*.b', 'x.a.b']); + + expect(norm(['!*.b', 'a.b', 'x.b', 'y.b', 'z.b'])).toEqual(['a.b', 'x.b', 'y.b', 'z.b']); + expect(norm(['!x', 'x.b', 'x.b.c', '!*.b.c'])).toEqual(['x.b']); + expect(norm(['!x', 'x.y', 'x.y.z', 'o.y.z', '!*.y.z', '!a', 'a.b', 'q.b.c', '!*.b.c'])).toEqual( + ['a.b', 'x.y', 'o.y.z', 'q.b.c', '!a.b.c'] + ); + expect(norm(['!x', 'x.b', 'x.b.c', '!*.b.c', '!*.*.c', '!f', '!d'])).toEqual(['x.b']); + + expect(norm(['!x', '!x.b', '!x.b.c', '*.b'])).toEqual(['*.b']); + + expect(norm(['!x', '!x.b', '!x.b.c', '*.b', '!*.b'])).toEqual([]); + expect(norm(['!*.b', 'x.b', 'x.b.c', 'x'])).toEqual(['x']); + expect(norm(['!*.b', 'x.b', 'x.b.c', 'x', 'y.b.c'])).toEqual(['x', 'y.b.c']); + + expect(norm(['*.b', '!a.b', '!x.b', '!y.b', '!z.b'])).toEqual([ + '*.b', + '!a.b', + '!x.b', + '!y.b', + '!z.b' + ]); + expect(norm(['*.b', 'x', '!*.b', 'y'])).toEqual(['x', 'y', '!x.b', '!y.b']); + + // cannot have both object and array notations for root level + expect(() => norm(['a.b', '[1].b'])).toThrow(); + }); + + test('.normalize() restrictive = true', () => { + const norm = (globs: string | string[]): string[] => normalize(globs, true); + + expect(() => norm(['*.[*]'])).toThrow(); + expect(() => norm(['*.[*]', 'x'])).toThrow(); + expect(() => norm(['*[*]*[*]'])).toThrow(); + expect(() => norm(['*[*'])).toThrow(); + expect(() => norm(['*', 'x-1'])).toThrow(); + + expect(norm(['*'])).toEqual(['*']); + expect(norm(['*.*'])).toEqual(['*']); + expect(norm(['*.*.*'])).toEqual(['*']); + expect(norm(['*[*].*'])).toEqual(['*']); + expect(norm(['*[*].*[*]'])).toEqual(['*']); + expect(norm(['*', '*'])).toEqual(['*']); + expect(norm(['x', 'x'])).toEqual(['x']); + expect(norm(['[*]'])).toEqual(['[*]']); + expect(norm(['!*'])).toEqual([]); + expect(norm(['![*]'])).toEqual([]); + expect(norm(['*', '!*'])).toEqual([]); + expect(norm(['!*', '*'])).toEqual([]); + + expect(norm(['*', 'user', 'video'])).toEqual(['*']); + expect(norm(['!*', 'user', 'video'])).toEqual([]); + expect(norm(['user', 'video', '!*'])).toEqual([]); // order shouldn't matter + expect(norm(['!*', '!user', 'video'])).toEqual([]); + + expect(norm(['*', 'name', 'pwd', 'id'])).toEqual(['*']); + expect(norm(['name', 'pwd', 'id'])).toEqual(['id', 'name', 'pwd']); + expect(norm(['*', 'name', 'pwd', '!id'])).toEqual(['*', '!id']); + expect(norm(['user.*', '!user.pwd'])).toEqual(['user', '!user.pwd']); + expect(norm(['*', '!*.id'])).toEqual(['*', '!*.id']); + + expect(norm(['!*.id', 'x.id'])).toEqual([]); + expect(norm(['user', '!*.id', 'x.id'])).toEqual(['user', '!user.id']); + + expect(norm(['*', '!user.pwd'])).toEqual(['*', '!user.pwd']); + expect(norm(['*', 'user.*', '!user.pwd'])).toEqual(['*', '!user.pwd']); + + expect( + norm(['*', '!id', 'name', 'car.model', '!car.*', 'id', 'name', 'user', '!user.pwd']) + ).toEqual(['*', '!id', '!car.*', '!user.pwd']); + + expect(norm(['*', '!id', 'car.model', '!car.*', '!user.pwd'])).toEqual([ + '*', + '!id', + '!car.*', + '!user.pwd' + ]); + + expect(norm(['name', 'pwd', '!id'])).toEqual(['name', 'pwd']); + + expect(norm(['!x.*.*', '*', 'x.o', 'id'])).toEqual(['*', '!x.*.*']); + + expect(norm(['!a.*', 'a'])).toEqual(['a', '!a.*']); // Notation#filter() would return {} + // should be treated same as above + expect(norm(['!a[*]', 'a'])).toEqual(['a', '!a[*]']); // Notation#filter() would return [] + expect(norm(['*', 'a[*]', '!a[*]'])).toEqual(['*', '!a[*]']); + expect(norm(['a[*]', '!a[*]'])).toEqual(['a', '!a[*]']); + expect(norm(['a[*]', '!a'])).toEqual([]); + expect(norm(['!a', 'a[*]'])).toEqual([]); + expect(norm(['a[*]', '!a[0][1][2]'])).toEqual(['a', '!a[0][1][2]']); + expect(norm(['a[4]', 'a[*]'])).toEqual(['a']); + expect(norm(['a.*', 'a.*[*]'])).toEqual(['a']); + expect(norm(['a.*', 'a.*[2]'])).toEqual(['a']); + + expect(norm(['a.b', 'a.*[*]', 'a.c[2].*'])).toEqual(['a']); + + expect(norm(['!x', 'c[1]', '!c[*]', '*', '!d.e'])).toEqual(['*', '!x', '!c[*]', '!d.e']); + + expect(norm(['!id', 'name', 'car.model', '!car.*', 'id', '!email'])).toEqual(['name']); + + expect(norm(['!y.*', 'x.x[1][0][*]', '*.x[*]', '!x.x[2][*]', 'a.b', 'c[*][1]'])).toEqual([ + '*.x', + 'a.b', + '!y.x', + 'c[*][1]', + '!x.x[2][*]' + ]); + + expect( + norm([ + 'bar.name', + '!bar.id', + 'bar', + 'foo.bar.baz', + 'foo.qux', + '!bar.id', + 'bar.id', + '!foo.*.baz' + ]) + ).toEqual(['bar', 'foo.qux', '!bar.id', '!foo.qux.baz']); + + expect(norm(['!*', 'a'])).toEqual([]); + expect(norm(['!*.b', 'a.b'])).toEqual([]); + expect(norm(['!a', 'a.b', 'x[*].*'])).toEqual(['x']); + expect(norm(['a.*', '!*.z'])).toEqual(['a', '!a.z']); + expect(norm(['x.*', '!*.y'])).toEqual(['x', '!x.y']); + expect(norm(['!a.x', 'a.x', '!a.*', '!*'])).toEqual([]); + expect(norm(['!a.*', '*.x'])).toEqual(['*.x', '!a.x']); + expect(norm(['!a.*', '*.x', '*.y'])).toEqual(['*.x', '*.y', '!a.x', '!a.y']); + expect(norm(['a', 'b.c', '!x', '*.y'])).toEqual(['a', '*.y', 'b.c', '!x.y']); + expect(norm(['a', 'b.c', '!x', '!*.y'])).toEqual(['a', 'b.c', '!a.y']); + expect(norm(['*', 'a', '!x', '!*.y'])).toEqual(['*', '!x', '!*.y']); + expect(norm(['!a.b.*', 'a.b.c'])).toEqual([]); + expect(norm(['!a.*', '!a.b.*', 'a.b.c'])).toEqual([]); + + expect(norm(['!x.*', 'x.y'])).toEqual([]); + expect(norm(['x.*', '!*.y'])).toEqual(['x', '!x.y']); + expect(norm(['a', 'x', '!a.z', '!x.y'])).toEqual(['a', 'x', '!a.z', '!x.y']); + expect(norm(['*', '!*.b', 'x.b', 'a'])).toEqual(['*', '!*.b']); + expect(norm(['*.x', '!*.*.b', 'x.a.b'])).toEqual(['*.x', '!*.x.b']); + expect(norm(['*', '*.x', '!*.*.b', 'x.a.b'])).toEqual(['*', '!*.*.b']); + + expect(norm(['!*.b', 'a.b', 'x.b', 'y.b', 'z.b'])).toEqual([]); + expect(norm(['!x', 'x.b', 'x.b.c', '!*.b.c'])).toEqual([]); + expect(norm(['!x', 'x.y', 'x.y.z', 'o.y.z', '!*.y.z', '!a', 'a.b', 'q.b.c', '!*.b.c'])).toEqual( + [] + ); + expect(norm(['!x', 'x.b', 'x.b.c', '!*.b.c', '!*.*.c', '!f', '!d'])).toEqual([]); + + expect(norm(['!x', '!x.b', '!x.b.c', '*.b'])).toEqual(['*.b', '!x.b']); + expect(norm(['!x', '*.b'])).toEqual(['*.b', '!x.b']); + expect(norm(['!x.b.c', '*.b'])).toEqual(['*.b', '!x.b.c']); + + expect(norm(['!x', '!x.b', '!x.b.c', '*.b', '!*.b'])).toEqual([]); + expect(norm(['!*.b', 'x.b', 'x.b.c', 'x'])).toEqual(['x', '!x.b']); + expect(norm(['!*.b', 'x.b', 'x.b.c', 'x', 'y.b.c'])).toEqual(['x', '!x.b']); + + expect(norm(['*.b', '!a.b', '!x.b', '!y.b', '!z.b'])).toEqual([ + '*.b', + '!a.b', + '!x.b', + '!y.b', + '!z.b' + ]); + expect(norm(['*.b', 'x', '!*.b', 'y'])).toEqual(['x', 'y', '!x.b', '!y.b']); + }); + + test('.normalize() » issue #7', () => { + const globs = ['!pass', '!password.prop', '*', '!password', '!password_reset_code']; + // console.log(normalize(globs)); + expect(normalize(globs)).toEqual(['*', '!pass', '!password', '!password_reset_code']); + }); + + test('.union() restrictive = false', () => { + const uni = (a: string[], b: string[]): string[] => union(a, b, false); + + expect(uni([], ['*', 'x.y'])).toEqual(['*']); + expect(uni(['*', 'x.y'], [])).toEqual(['*']); + expect(uni([], ['!x.*', 'x.y'])).toEqual(['x.y']); + expect(uni(['!x.*', 'x.y'], [])).toEqual(['x.y']); + expect(uni(['a.b', '*.z'], ['!x.*', 'x.y'])).toEqual(['*.z', 'a.b', 'x.y']); + + expect(uni(['*', 'a', 'b', '!id', '!x.*'], ['*', '!b', 'id', '!pwd', 'x.o'])).toEqual(['*']); + + expect(uni(['*', '!id', '!x.*'], ['*', 'id', '!pwd', '!x.*', 'x.o'])).toEqual([ + '*', + '!x.*', + 'x.o' + ]); + + expect(uni(['*', '!x.*'], ['!x.*.*'])).toEqual(['*', '!x.*']); + expect(uni(['*', '!x.*'], ['*', '!x.*.*'])).toEqual(['*', '!x.*.*']); + expect(uni(['*', '!x.y'], ['!x.y.z'])).toEqual(['*', '!x.y']); + expect(uni(['*', '!x.y'], ['*', '!x.y.z'])).toEqual(['*', '!x.y.z']); + + expect(uni(['*', '!x.*'], ['*', '!x.*.*'])).toEqual(['*', '!x.*.*']); + + expect(uni(['*', '!id', '!x.*'], ['*', 'id', '!pwd', '!x.*.*', 'x.o'])).toEqual([ + '*', + '!x.*.*' + ]); + + expect(uni(['*', '!x[*]'], ['*', '!x[4]', 'x[1]'])).toEqual(['*', '!x[4]']); + + expect(uni(['*', 'a', 'b[2]', '!id', '!x[*]'], ['*', '!b[*]', 'id', '!x[4]', 'x[1]'])).toEqual([ + '*', + '!x[4]' + ]); + + expect(uni(['*[*]', '!a[1]', '!x[*]'], ['*', 'a[1]', '!b[*]', '!x[*]', 'x[0]'])).toEqual([ + '*', + '!x[*]', + 'x[0]' + ]); + + expect(uni(['*', '!a[*]', '!x[*]'], ['*', 'a[*]', '!b', '!x[*].*', 'x[1]'])).toEqual([ + '*', + '!x[*].*' + ]); + + expect(uni(['*', 'a', 'b[2]', '!id', '!x[*]'], ['*', '!b[*]', 'id', '!x[4]', 'x[1]'])).toEqual([ + '*', + '!x[4]' + ]); + + expect(uni(['*', '!a[*]', '!x[*]'], ['*', 'a[*]', '!b', '!x[*].*', 'x[1]'])).toEqual([ + '*', + '!x[*].*' + ]); + + expect(uni(['a.*', '!*.z'], ['x.*', '!*.y'])).toEqual(['a', 'x', '!a.z', '!x.y']); + expect(uni(['*', 'a.*', '!*.z'], ['x.*', '!*.y'])).toEqual(['*', '!*.z']); + expect(uni(['*', 'a.*', '!*.z'], ['*', 'x.*', '!*.y'])).toEqual(['*']); + + // cannot union an obj-glob with an arr-glob + expect(() => union(['*'], ['[*]'])).toThrow(); + }); + + test('.union() restrictive = true', () => { + const uni = (a: string[], b: string[]): string[] => union(a, b, true); + + expect(uni([], ['*', 'x.y'])).toEqual(['*']); + expect(uni(['*', 'x.y'], [])).toEqual(['*']); + expect(uni([], ['!x.*', 'x.y'])).toEqual([]); + expect(uni(['!x.*', 'x.y'], [])).toEqual([]); + expect(uni(['a.b', '*.z'], ['!x.*', 'x.y'])).toEqual(['*.z', 'a.b']); + + expect(uni(['*', 'a', 'b', '!id', '!x.*'], ['*', '!b', 'id', '!pwd', 'x.o'])).toEqual(['*']); + + expect(uni(['*', '!id', '!x.*'], ['*', 'id', '!pwd', '!x.*', 'x.o'])).toEqual(['*', '!x.*']); + + expect(uni(['*', '!x.*'], ['!x.*.*'])).toEqual(['*', '!x.*']); + + expect(uni(['*', '!x.*'], ['*', '!x.*.*'])).toEqual(['*', '!x.*.*']); + + expect(uni(['*', '!id', '!x.*'], ['*', 'id', '!pwd', '!x.*.*', 'x.o'])).toEqual([ + '*', + '!x.*.*' + ]); + + expect(uni(['*', '!x[*]'], ['*', '!x[4]', 'x[1]'])).toEqual(['*', '!x[4]']); + + expect(uni(['*', 'a', 'b[2]', '!id', '!x[*]'], ['*', '!b[*]', 'id', '!x[4]', 'x[1]'])).toEqual([ + '*', + '!x[4]' + ]); + + expect(uni(['*[*]', '!a[1]', '!x[*]'], ['*', 'a[1]', '!b[*]', '!x[*]', 'x[0]'])).toEqual([ + '*', + '!x[*]' + ]); + + expect(uni(['*', '!a[*]', '!x[*]'], ['*', 'a[*]', '!b', '!x[*].*', 'x[1]'])).toEqual([ + '*', + '!x[*].*' + ]); + + expect(uni(['*', 'a', 'b[2]', '!id', '!x[*]'], ['*', '!b[*]', 'id', '!x[4]', 'x[1]'])).toEqual([ + '*', + '!x[4]' + ]); + + expect(uni(['*[*]', '!a[1]', '!x[*]'], ['*', 'a[1]', '!b[*]', '!x[*]', 'x[0]'])).toEqual([ + '*', + '!x[*]' + ]); + + expect(uni(['*', '!a[*]', '!x[*]'], ['*', 'a[*]', '!b', '!x[*].*', 'x[1]'])).toEqual([ + '*', + '!x[*].*' + ]); + + expect(uni(['a.*', '!*.z'], ['x.*', '!*.y'])).toEqual(['a', 'x', '!a.z', '!x.y']); + }); + + test('.union() check mutation, order', () => { + const globA = ['foo.bar.baz', 'bar.*', '!bar.id', 'bar.name', '!foo.qux.boo']; + const globB = ['!foo.*.baz', 'bar.id', 'bar.name', '!bar.*', 'foo.qux.*']; + // normalized to: + // globA = [ 'bar', '!bar.id', 'foo.bar.baz' ] + // globB = [ 'foo.qux', '!foo.qux.baz' ] + + // for checking mutation + const cloneGlobA = globA.concat(); + const cloneGlobB = globB.concat(); + + expect(union(globA, globB, true)).toEqual([ + 'bar', + 'foo.qux', + '!bar.id', + 'foo.bar.baz', + '!foo.qux.baz' + ]); + + // should not mutate given globs arrays + expect(globA).toEqual(cloneGlobA); + expect(globB).toEqual(cloneGlobB); + + // order of parameters should not matter + expect(union(['*'], ['!id'])).toEqual(['*']); + expect(union(['!id'], ['*'])).toEqual(['*']); + expect(union(['id'], ['*'])).toEqual(['*']); + expect(union(['*', '!id'], ['*'])).toEqual(['*']); + expect(union(['*'], ['*', '!id'])).toEqual(['*']); + expect(union(['*'], ['!a[*]'])).toEqual(['*']); + expect(union(['!a[*]'], ['*'])).toEqual(['*']); + expect(union(['a[1]'], ['*'])).toEqual(['*']); + expect(union(['*'], ['a[1]'])).toEqual(['*']); + expect(union(['a[*]', '!a[0]'], ['a[0][*]'])).toEqual(['a']); + expect(union(['a[0][*]'], ['a[*]', '!a[0]'])).toEqual(['a']); + + const a = ['*', '!id']; + const b = ['*', '!pwd']; + const c = ['email']; + const d = ['*']; + // const e = ['*', '!id', '!pwd']; + + expect(union(b, c)).toEqual(['*', '!pwd']); + expect(union(c, d)).toEqual(['*']); + expect(union(a, d)).toEqual(['*']); + expect(union(c, d)).toEqual(['*']); + + expect(union(['*', 'a[*]', '!b[2]', '!x[*]', 'o'], ['*', 'b[2]', '!pwd', 'x[5]', 'o'])).toEqual( + ['*'] + ); + + expect(union(['*', 'email', '!id', '!x.*', 'o'], ['*', 'id', '!pwd', 'x.name', 'o'])).toEqual([ + '*' + ]); + + expect( + union( + ['user.*', '!user.email', 'car.model', '!*.id'], + ['!*.date', 'user.email', 'car', '*.age'] + ) + ).toEqual(['car', 'user', '*.age', '!car.date', '!user.id']); + }); + + test('.union() (by intersection)', () => { + let u; + u = union(['*', '!*.z'], ['*', '!x.*']); + expect(u).toEqual(['*', '!x.z']); + u = union(['*', '!*[2]'], ['*', '!x[*]']); + expect(u).toEqual(['*', '!x[2]']); + + // intersection in same (normalize) + expect(union(['a.*', '!*.z'], ['x.*', '!*.y'])).toEqual(['a', 'x', '!a.z', '!x.y']); + expect(union(['a', '!*.z'], ['x', '!*.y'])).toEqual(['a', 'x', '!a.z', '!x.y']); + expect(union(['a.b.*', '!*.*.z'], ['x.*', '!*.y'])).toEqual(['x', 'a.b', '!x.y', '!a.b.z']); + expect(union(['a.b.*', '!*.*.z'], ['x', 'a.b.z', '!*.y'])).toEqual(['x', 'a.b', '!x.y']); + }); +}); diff --git a/test/ac.spec.ts b/test/ac.spec.ts new file mode 100644 index 0000000..e2bc3d7 --- /dev/null +++ b/test/ac.spec.ts @@ -0,0 +1,82 @@ +import { Notation } from '../src/core/Notation.js'; +import { NotationGlob } from '../src/core/NotationGlob.js'; + +const notate = Notation.create; +const { union } = NotationGlob; + +describe('ac', () => { + test('#filter()', () => { + let globs = ['*', '!account.balance.credit', '!account.id', '!secret']; + const data = { + name: 'Company, LTD.', + address: { + city: 'istanbul', + country: 'TR' + }, + account: { + id: 33, + taxNo: 12345, + balance: { + credit: 100, + deposit: 0 + } + }, + secret: { + value: 'hidden' + } + }; + let filtered = notate(data).filter(globs).value; + expect(filtered.name).toEqual(expect.any(String)); + expect(filtered.address).toEqual(expect.any(Object)); + expect(filtered.address.city).toEqual('istanbul'); + expect(filtered.account).toBeDefined(); + expect(filtered.account.id).toBeUndefined(); + expect(filtered.account.balance).toBeDefined(); + expect(filtered.account.credit).toBeUndefined(); + expect(filtered.secret).toBeUndefined(); + + filtered = notate(data).filter('!*').value; + expect(filtered).toEqual({}); + + // filtering array of objects + globs = ['*', '!id']; + const data2 = [ + { id: 1, name: 'x', age: 30 }, + { id: 2, name: 'y', age: 31 }, + { id: 3, name: 'z', age: 32 } + ]; + filtered = notate(data2).filter(globs).value; + expect(filtered).toEqual(expect.any(Array)); + expect(filtered.length).toEqual(data2.length); + }); + + test('#filter() 2', () => { + const o = { + name: 'John', + age: 30, + account: { + id: 1, + country: 'US' + } + }; + const filtered = notate(o).filter(['*', '!account.id', '!age']).value; + expect(filtered.name).toEqual('John'); + expect(filtered.account.id).toBeUndefined(); + expect(filtered.account.country).toEqual('US'); + + expect(o.account.id).toEqual(1); + expect(o).not.toEqual(filtered); + }); + + test('#union()', () => { + const globA = ['*', '!id', '!pwd']; + const globB = ['*', '!pwd', 'title']; + expect(union(globA, globB)).toEqual(['*', '!pwd']); + + let u = union(['image', 'name'], ['name', '!location']); + u = union(u, ['*', '!location']); + expect(u).toEqual(['*', '!location']); + u = union(['*'], u); + expect(u).toEqual(['*']); + }); +}); diff --git a/test/bracket.spec.ts b/test/bracket.spec.ts new file mode 100644 index 0000000..88c8fc2 --- /dev/null +++ b/test/bracket.spec.ts @@ -0,0 +1,358 @@ +/* eslint-disable no-sparse-arrays */ +/* eslint-disable no-console */ +/* eslint-disable prefer-arrow/prefer-arrow-functions */ + +import type { INotationOptions } from '../src/core/INotationOptions.js'; + +import { Notation } from '../src/core/Notation.js'; +import type { Collection, UnknownObject } from '../src/types.js'; + +/** + * Test Suite: Notation + * @module notation.spec + */ +describe('Test Suite: Array/Bracket notation', () => { + const objSource = { + arr: [ + { x: { y: 0 }, z: 3 }, + { x: { y: 1 }, z: 4 }, + { x: { y: 2 }, z: 5 } + ], + de: { + e: { + p: [{ x: { y: [0, null] } }, { x: { y: [1, 3] } }, { x: { y: [2, 4] } }] + } + } + }; + + const arrInArr = { + x: [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8] + ], + y: { + z: [ + [0, 1, 2], + [[3], [4], [5]] + ] + } + }; + + function create(data: Collection, opts?: INotationOptions): Notation { + return Notation.create(structuredClone(data), opts); + } + + function fnFilter(data: Collection) { + return (globs: string | string[]): Notation => create(data).filter(globs); + } + + test('Notation .isvalid()', () => { + expect(Notation.isValid('a.[0]')).toEqual(false); + expect(Notation.isValid('a[0]b')).toEqual(false); + expect(Notation.isValid('a[0].b')).toEqual(true); + expect(Notation.isValid('a[0].b[0]')).toEqual(true); + expect(Notation.isValid('a[0].b[0].c')).toEqual(true); + expect(Notation.isValid('a[0].b[0].c.d')).toEqual(true); + expect(Notation.isValid('a[0].b[0].c.d[2]')).toEqual(true); + expect(Notation.isValid('a[0].b[0].c.d[2].x[2][1]')).toEqual(true); + expect(Notation.isValid('x[0][10][2][5][3][4][200][0].y')).toEqual(true); + }); + + test('Notation .countNotes(), .parent(), .first(), .last()', () => { + const pattern = 'arr[0].x[1][0].y[5]'; + expect(Notation.countNotes(pattern)).toEqual(7); + expect(Notation.parent(pattern)).toEqual('arr[0].x[1][0].y'); + expect(Notation.parent('arr[0]')).toEqual('arr'); + expect(Notation.first(pattern)).toEqual('arr'); + expect(Notation.last(pattern)).toEqual('[5]'); + }); + + // test('Notation .eachNote()', () => { + // const pattern = 'arr[0].x[1][0].y[5]'; + // const result = { + // 0: { levelNota: 'arr', note: 'arr' }, + // 1: { levelNota: 'arr[0]', note: '[0]' }, + // 2: { levelNota: 'arr[0].x', note: 'x' }, + // 3: { levelNota: 'arr[0].x[1]', note: '[1]' }, + // 4: { levelNota: 'arr[0].x[1][0]', note: '[0]' }, + // 5: { levelNota: 'arr[0].x[1][0].y', note: 'y' }, + // 6: { levelNota: 'arr[0].x[1][0].y[5]', note: '[5]' } + // }; + // let c = 0; + // Notation.eachNote(pattern, (levelNota, note, index) => { + // const expected = result[index]; + // expect(levelNota).toEqual(expected.levelNota); + // expect(note).toEqual(expected.note); + // c++; + // }); + // expect(c).toEqual(Notation.countNotes(pattern)); + // }); + + test('Notation #each()', () => { + const nota = Notation.create({ a: [{ x: 1, y: 2 }], b: { c: [3, [4], [, { z: 5 }]] } }); + const result = [ + { notation: 'a[0].x', key: 'x', value: 1 }, + { notation: 'a[0].y', key: 'y', value: 2 }, + { notation: 'b.c[0]', key: '[0]', value: 3 }, + { notation: 'b.c[1][0]', key: '[0]', value: 4 }, + { notation: 'b.c[2][0]', key: '[0]', value: undefined }, + { notation: 'b.c[2][1].z', key: 'z', value: 5 } + ]; + let i = 0; + nota.each((notation, key, value, _object) => { + const expected = result[i]; + expect(notation).toEqual(expected.notation); + expect(key).toEqual(expected.key); + expect(value).toEqual(expected.value); + i++; + // console.log(notation, ',', key, ',', value); + }); + expect(i).toEqual(6); + }); + + test('Notation #eachValue()', () => { + const dArr = [, { z: 5 }]; + const cArr = [3, [4], dArr]; + const bObj = { c: cArr }; + const nota = create({ a: 9, b: bObj }); + // results for b.c[2] iteration + const result = { + 0: { + levelValue: bObj, + notation: 'b', + key: 'b' + }, + 1: { + levelValue: cArr, + notation: 'b.c', + key: 'c' + }, + 2: { + levelValue: dArr, + notation: 'b.c[2]', + key: '[2]' + } + }; + let i = 0; + nota.eachValue('b.c[2]', (levelValue, notation, key) => { + const expected = result[i]; + // console.log('>>> eachValue:', i, notation, key); + // console.log('>>> levelValue:', levelValue, expected.levelValue); + expect(levelValue).toEqual(expected.levelValue); + expect(notation).toEqual(expected.notation); + expect(key).toEqual(expected.key); + i++; + }); + expect(i).toEqual(3); + }); + + test('Notation #getNotations()', () => { + const nota = create({ a: [{ x: 1, y: 2 }], b: { c: [3, [4], [, { z: 5 }]] } }); + expect(nota.getNotations()).toEqual([ + 'a[0].x', + 'a[0].y', + 'b.c[0]', + 'b.c[1][0]', + 'b.c[2][0]', + 'b.c[2][1].z' + ]); + }); + + test('Notation #get()', () => { + let nota = create(objSource); + expect(nota.get('arr[0].x')).toEqual({ y: 0 }); + expect(nota.get('arr[1].x.y')).toEqual(1); + expect(nota.get('arr[2]')).toEqual({ x: { y: 2 }, z: 5 }); + + expect(nota.get('de.e.p[0].x')).toEqual({ y: [0, null] }); + expect(nota.get('de.e.p[1].x.y')).toEqual([1, 3]); + expect(nota.get('de.e.p[2].x.y[1]')).toEqual(4); + + // star not allowed in Notation class (except #filter method) + expect(() => nota.get('de.e.p[*].x')).toThrow(); + + nota = create(arrInArr); + expect(nota.get('x[1][1]')).toEqual(4); + expect(nota.get('x[2][0]')).toEqual(6); + expect(nota.get('y.z[0]')).toEqual([0, 1, 2]); + expect(nota.get('y.z[0][2]')).toEqual(2); + expect(nota.get('y.z[1][1][0]')).toEqual(4); + + // TODO: we should support notation starting with brackets (i.e. array + // as source). but this is not crucial for this PR. + + // nota = create([{ x: 1 }, { x: 2 }, { x: 3 }]); + // expect(nota.get('[1].x')).toEqual(2); // currently fails + }); + + test('Notation #set(), #has(), #hasDefined()', () => { + let nota = create({}); + + nota.set('x.y[1]', { z: 1 }); + expect(nota.get('x.y[0]')).toEqual(undefined); + expect(nota.value.x.y[0]).toEqual(undefined); + expect(nota.has('x.y[0]')).toEqual(true); + expect(nota.hasDefined('x.y[0]')).toEqual(false); + expect(nota.get('x.y[1].z')).toEqual(1); + expect(nota.value.x.y[1].z).toEqual(1); + nota.set('x.y[0]', { z: 5 }); + expect(nota.get('x.y[0].z')).toEqual(5); + nota.set('x.y[2].a.b.c[2]', [1, 2, 3]); + expect(nota.get('x.y[2].a')).toEqual({ b: { c: [, , [1, 2, 3]] } }); + + nota = create({}).set('x.y[1]', undefined); + expect(nota.has('x.y[1]')).toEqual(true); + expect(nota.hasDefined('x.y[1]')).toEqual(false); + expect(nota.has('x.y[0]')).toEqual(true); + expect(nota.hasDefined('x.y[0]')).toEqual(false); + expect(nota.has('x.y[2]')).toEqual(false); + expect(nota.hasDefined('x.y[2]')).toEqual(false); + + // TODO: source as an array. we'll add support later... + // nota = create([]); + // nota.set('[1].x', 'test'); + // expect(nota.get('[0]')).toEqual(undefined); + // expect(nota.get('[1]')).toEqual({ x: 'test' }); + }); + + test('Notation #merge()', () => { + const nota = create({ a: [{ x: 1 }], b: { c: [3] } }); + let merged = nota.merge({ a: [{ y: 2 }] }, false); + expect(merged.value.a[0]).toEqual({ x: 1 }); + merged = nota.merge({ a: [{ y: 2 }] }, true); + expect(merged.value.a[0]).toEqual({ y: 2 }); + merged = nota.merge({ b: { c: [4, 5] } }, true); + expect(merged.value.b.c).toEqual([4, 5]); + }); + + test('Notation #separate()', () => { + const obj = { a: [{ x: 1 }], b: { c: [1, 2] } }; + const nota = create(obj); + const separated = nota.separate(['b.c[1]', 'a[0].x']); + expect(separated.b.c[1]).toEqual(2); + expect(separated.a[0].x).toEqual(1); + expect(nota.value.b.c).toEqual([1]); + expect(nota.value.a[0]).toEqual({}); + }); + + test('Notation #remove()', () => { + const nota = create({ a: [{ x: 1, y: 2 }], b: { c: [3, 4, 5] } }); + expect(nota.remove('a[0].y').value.a[0]).toEqual({ x: 1 }); + expect(nota.remove('b.c[1]').value.b.c).toEqual([3, 5]); + // console.log(nota.value); + }); + + test('Notation #copyTo()', () => { + const nota = create({ a: [{ x: 1, y: 2 }], b: { c: [3, 4, 5] } }); + let dest = {}; + nota.copyTo(dest, 'a[0].y'); + expect(dest).toEqual({ a: [{ y: 2 }] }); + dest = { x: [] }; + nota.copyTo(dest, 'a[0].y', 'x[1].z'); + expect(dest).toEqual({ x: [, { z: 2 }] }); + }); + + test('Notation #copyFrom()', () => { + const nota = create({ a: [{ x: 1, y: 2 }], b: { c: [3, 4, 5] } }); + const dest = { e: [1, { f: [6] }] }; + nota.copyFrom(dest, 'e[1].f[0]', 'a[0].z'); + expect(nota.value.a[0].z).toEqual(6); + }); + + test('Notation #moveTo()', () => { + const nota = create({ a: [{ x: 1, y: 2 }], b: { c: [3, 4, 5] } }); + let dest = {}; + nota.moveTo(dest, 'a[0].y'); + expect(nota.value.a).toEqual([{ x: 1 }]); + expect(dest).toEqual({ a: [{ y: 2 }] }); + dest = { x: [] }; + nota.moveTo(dest, 'b.c[2]', 'x[1].z'); + expect(dest).toEqual({ x: [, { z: 5 }] }); + expect(nota.value.b.c).toEqual([3, 4]); + }); + + test('Notation #moveFrom()', () => { + const nota = create({ a: [{ x: 1, y: 2 }], b: { c: [3, 4, 5] } }); + const dest: UnknownObject = { e: [1, { f: [6] }] }; + nota.moveFrom(dest, 'e[1].f[0]', 'a[0].z'); + expect(nota.value.a[0].z).toEqual(6); + expect(dest.e[1].f).toEqual([]); + }); + + test('Notation #extract() xxx', () => { + const nota = create({ a: [{ x: 1, y: 2 }], b: { c: [3, 4, 5] } }); + let extracted = nota.extract('a[0].y'); + expect(extracted.a).toEqual([{ y: 2 }]); + expect(nota.value.a).toEqual([{ x: 1, y: 2 }]); + extracted = nota.extract('b.c[1]'); + expect(extracted.b.c).toEqual([, 4]); + expect(nota.value.b.c).toEqual([3, 4, 5]); + + const nota2 = create({ a: [{ x: 1, y: 2 }], b: { c: [3, 4, 5] } }); + extracted = nota2.extract('b.c[1]'); + expect(extracted.b.c).toEqual([undefined, 4]); + extracted = nota2.extract('b.c[1]', 'b.c[0]'); + expect(extracted.b.c).toEqual([4]); + }); + + test('Notation #extrude()', () => { + const opts = { preserveIndices: false }; + const nota = create({ a: [{ x: 1, y: 2 }], b: { c: [3, 4, 5] } }, opts); + let extruded = nota.extrude('a[0].y'); + expect(extruded.a).toEqual([{ y: 2 }]); + expect(nota.value.a).toEqual([{ x: 1 }]); + + extruded = nota.extrude('b.c[1]'); + expect(extruded.b.c).toEqual([undefined, 4]); + expect(nota.value.b.c).toEqual([3, 5]); + // console.log(JSON.stringify(nota.value.b.c)); + }); + + test('Notation #flatten()', () => { + const nota = create({ a: [{ x: 1, y: 2 }], b: { c: [3, [4], [, { z: 5 }]] } }); + const expected = { + 'a[0].x': 1, + 'a[0].y': 2, + 'b.c[0]': 3, + 'b.c[1][0]': 4, + 'b.c[2][0]': undefined, + 'b.c[2][1].z': 5 + }; + expect(nota.flatten().value).toEqual(expected); + }); + + test('Notation #filter()', () => { + const o = { ...objSource }; + o.arr[2].x.y = 8; + const filter = fnFilter(o); + + // expect(filter('arr[1].x').value).toEqual({ arr: [undefined, { x: { y: 1 } }] }); + // // console.log(JSON.stringify(filter('arr[1].x').value)); + // expect(filter('arr[*].z').value).toEqual({ arr: [{ z: 3 }, { z: 4 }, { z: 5 }] }); + // expect((filter('arr[*].x').value as UnknownObject).arr.length).toEqual(3); + expect(filter('arr[*].x').value).toEqual({ + arr: [{ x: { y: 0 } }, { x: { y: 1 } }, { x: { y: 8 } }] + }); + + const o1 = { a: [1, 2, 3], b: [] }; + expect(create(o1).filter('a[2]').value).toEqual({ a: [, , 3] }); + expect(create(o1).filter('a[1]').value).toEqual({ a: [, 2] }); + expect(create(o1).filter('b[*]').value).toEqual({ b: [] }); + expect(create(o1).filter('b[1]').value).toEqual({}); + + const o2 = { a: [[1], { x: [{}, { y: 2 }, 3] }], b: [{ c: [4, 5, 6] }] }; + expect(create(o2).filter('a[2]').value).toEqual({}); + expect(create(o2).filter('a[1]').value).toEqual({ a: [, { x: [{}, { y: 2 }, 3] }] }); + expect(create(o2).filter('a[1].x[1]').value).toEqual({ a: [, { x: [, { y: 2 }] }] }); + expect(create(o2).filter('a[1].x[1].y').value).toEqual({ a: [, { x: [, { y: 2 }] }] }); + expect(create(o2).filter('a[1].x[2]').value).toEqual({ a: [, { x: [, , 3] }] }); + expect(create(o2).filter('a[1].x[0]').value).toEqual({ a: [, { x: [{}] }] }); + expect(create(o2).filter('a[1].x[*]').value).toEqual({ a: [, { x: [{}, { y: 2 }, 3] }] }); + + // TODO: below returns { a: [, { x: [{ y: 2 }, 3] }] } » no empty first item in x array. + // this is ok for now but should be consistent with others (empty the item to preserve the index) + // expect(create(o2).filter(['a[1].x[*]', '!a[1].x[0]']).value).toEqual({ a: [, { x: [, { y: 2 }, 3] }] }); + // console.log(JSON.stringify(create(o2).filter(['a[1].x[*]', '!a[1].x[0]']).value)); + }); +}); diff --git a/test/edge.spec.ts b/test/edge.spec.ts new file mode 100644 index 0000000..e9d2fb7 --- /dev/null +++ b/test/edge.spec.ts @@ -0,0 +1,60 @@ +/* Targeted edge-case coverage for branches/functions not hit by the main suites. */ + +import { Notation } from '../src/core/Notation.js'; +import { NotationError } from '../src/core/NotationError.js'; +import { NotationGlob } from '../src/core/NotationGlob.js'; +import { utils } from '../src/utils.js'; + +describe('edge cases', () => { + test('utils.cleanSparseArray() removes holes (and undefined)', () => { + const out = utils.cleanSparseArray([0, , 1, , undefined, , null]); + expect(out).toEqual([0, 1, null]); + }); + + test('NotationGlob.join() builds glob and validates notes', () => { + expect(NotationGlob.join(['x', 'y', 'z'])).toBe('x.y.z'); + expect(NotationGlob.join(['x', '*'])).toBe('x.*'); + // bracket note is appended without a separating dot + expect(NotationGlob.join(['x', '[0]'])).toBe('x[0]'); + // normalize removes trailing wildcards + expect(NotationGlob.join(['x', '*'], true)).toBe('x'); + // an empty/invalid note throws + expect(() => NotationGlob.join(['x', ''])).toThrow(NotationError); + }); + + test('NotationGlob.isValidNote()', () => { + expect(NotationGlob.isValidNote('x')).toBe(true); + expect(NotationGlob.isValidNote('*')).toBe(true); + expect(NotationGlob.isValidNote('[0]')).toBe(true); + expect(NotationGlob.isValidNote('')).toBe(false); + expect(NotationGlob.isValidNote(123 as unknown as string)).toBe(false); + }); + + test('NotationGlob#covers() accepts an array of notes', () => { + const g = NotationGlob.create('x.*'); + expect(g.covers(['x', 'y'])).toBe(true); + }); + + test('Notation#set() resets a non-collection parent when going deeper', () => { + const n = Notation.create>({}).set('a.b', 1); + // a.b is currently a primitive (1); setting a.b.c must reset it to an object + n.set('a.b.c', 2); + expect(n.get('a.b.c')).toBe(2); + }); + + test('Notation#set() resets a non-collection parent to an array for a numeric note', () => { + const n = Notation.create>({}).set('a.b', 1); + // a.b is a primitive (1); setting a.b[0] must reset it to an array + n.set('a.b[0]', 2); + expect(n.get('a.b[0]')).toBe(2); + expect(Array.isArray(n.get('a.b'))).toBe(true); + }); + + test('Notation#filter() with negated object- and array-wildcard globs', () => { + const obj = Notation.create({ a: { b: 1, c: 2 }, d: 3 }).filter(['*', '!a.*']).value; + expect(obj).toEqual({ a: {}, d: 3 }); + + const arr = Notation.create({ a: [1, 2, 3], d: 3 }).filter(['*', '!a[*]']).value; + expect(arr).toEqual({ a: [], d: 3 }); + }); +}); diff --git a/test/errors.spec.ts b/test/errors.spec.ts new file mode 100644 index 0000000..94509ba --- /dev/null +++ b/test/errors.spec.ts @@ -0,0 +1,43 @@ +/* Asserts thrown error *messages* (not just that something throws), to pin the + message constants against mutation. */ + +import { Notation } from '../src/core/Notation.js'; +import { NotationGlob } from '../src/core/NotationGlob.js'; + +const create = Notation.create; + +describe('error messages', () => { + test('invalid source', () => { + expect(() => new Notation(1 as unknown as object)).toThrow(/Invalid source/); + expect(() => new Notation('x' as unknown as object)).toThrow(/Invalid source/); + }); + + test('invalid destination (copyTo / moveTo)', () => { + expect(() => create({ a: 1 }).copyTo(1 as unknown as object, 'a')).toThrow( + /Invalid destination/ + ); + expect(() => create({ a: 1 }).moveTo(1 as unknown as object, 'a')).toThrow( + /Invalid destination/ + ); + }); + + test('invalid notations object (merge / separate)', () => { + // source is an array -> notations object must be an array + expect(() => create([1]).merge({} as unknown as unknown[])).toThrow( + /Invalid notations object\. Expected an array/ + ); + // source is an object -> notations object must be an object + expect(() => create({}).merge([] as unknown as object)).toThrow( + /Invalid notations object\. Expected an object/ + ); + expect(() => create([1]).separate('x' as unknown as string[])).toThrow( + /Invalid notations object\. Expected an array/ + ); + }); + + test('invalid glob notation', () => { + expect(() => new NotationGlob('a..b')).toThrow(/Invalid glob notation/); + expect(() => NotationGlob.normalize(['a..b'])).toThrow(/Invalid glob notation/); + expect(() => NotationGlob.split('a..b')).toThrow(/Invalid glob notation/); + }); +}); diff --git a/test/filter.spec.ts b/test/filter.spec.ts new file mode 100644 index 0000000..644755f --- /dev/null +++ b/test/filter.spec.ts @@ -0,0 +1,431 @@ +/* eslint camelcase:0, max-lines-per-function:0, consistent-return:0, max-statements:0, max-lines:0, max-len:0 */ + +import { Notation } from '../src/core/Notation.js'; +import type { UnknownObject } from '../src/types.js'; +import { utils } from '../src/utils.js'; + +const notate = Notation.create; + +const o = { + name: 'onur', + age: 36, + account: { + id: 15, + tags: 20, + likes: ['movies', '3d', 'programming', 'music'] + }, + billing: { + account: { + id: 121, + credit: 300, + balance: -293 + } + }, + company: { + name: 'pilot co', + address: { + city: 'istanbul', + country: 'TR', + location: { + lat: 34.123123, + lon: 30.123123 + } + }, + account: { + id: 33, + taxNo: 12345 + }, + limited: true, + notDefined: undefined, + nuller: null, + zero: 0 + } +}; + +describe('Notation#filter()', () => { + test('#filter()', () => { + // var glob = create; + const nota = notate(o).clone(); + // console.log('value ---:', nota.value); + const globs = ['!company.limited', 'billing.account.credit', 'company.*', 'account.id']; + const filtered = nota.filter(globs).value; + // console.log('filtered ---:', filtered); + + expect(filtered.company.name).toBeDefined(); + expect(filtered.company.address).toBeDefined(); + expect(filtered.company.limited).toBeUndefined(); + expect(filtered.account.id).toBeDefined(); + expect(filtered.account.likes).toBeUndefined(); + expect(filtered.billing.account.credit).toBeDefined(); + + // original object should not be modified + // console.log(JSON.stringify(o, null, ' ')); + expect(o.company.name).toBeDefined(); + expect(o.company.limited).toBeDefined(); + expect(o.account.id).toBeDefined(); + + const assets = { + model: 'Onur', + phone: { brand: 'Apple', model: 'iPhone' }, + car: { brand: 'Ford', model: 'Mustang' } + }; + const n = notate(assets); + const m1 = n.filter('*').value; + const m2 = n.filter('*.*').value; + const m3 = n.filter('*.*.*').value; + expect(m1.model).toBeDefined(); + expect(m1.phone.model).toBeDefined(); + expect(m2.model).toBeDefined(); + expect(m2.phone.model).toBeDefined(); + expect(m3.model).toBeDefined(); + expect(m3.phone.model).toBeDefined(); + expect(Object.keys(m3).length).toEqual(3); + + // globs.push('*'); + // nota.filter(globs); + }); + + test('#filter() » 2nd level wildcard', () => { + const data = { + model: 'Onur', + phone: { brand: 'Apple', model: 'iPhone' }, + car: { brand: 'Ford', model: 'Mustang' } + }; + expect(notate(data).filter('phone.*').value).toEqual({ phone: data.phone }); + }); + + test('#filter() » negated object', () => { + const data = { + name: 'Onur', + phone: { brand: 'Apple', model: 'iPhone' }, + car: { brand: 'Ford', model: 'Mustang' } + }; + const globs = ['*', '!phone']; + const filtered = notate(data).filter(globs).value; + expect(filtered.name).toEqual(data.name); + expect(filtered.phone).toBeUndefined(); + expect(filtered.car).toBeDefined(); + // console.log(filtered); + // console.log(data); + }); + + test('#filter() » normal and negated of the same (negated should win)', () => { + const data = { prop: { id: 1, x: true }, y: true }; + // we have the same glob both as negated and normal. negated should win. + let globs = ['prop.id', '!prop.id']; + let filtered = notate(data).filter(globs).value; + expect(filtered.prop).toBeUndefined(); + expect(filtered.y).toBeUndefined(); + // add wildcard + globs = ['!prop.id', 'prop.id', '*']; + filtered = notate(data).filter(globs).value; + expect(utils.type(filtered.prop)).toEqual('object'); + expect(filtered.prop.id).toBeUndefined(); + expect(filtered.prop.x).toEqual(true); + expect(filtered.y).toEqual(data.y); + }); + + test('#filter() » with/out wildcard', () => { + const data = { name: 'Onur', id: 1 }; + // we have no wildcard '*' here. + let globs = ['!id']; + // should filter as `{}` + let filtered = notate(data).filter(globs).value; + expect(utils.type(filtered)).toEqual('object'); + expect(Object.keys(filtered).length).toEqual(0); + // add wildcard + globs = ['*', '!id']; + filtered = notate(data).filter(globs).value; + expect(filtered.name).toEqual(data.name); + expect(filtered.id).toBeUndefined(); + // no negated (id is duplicate in this case) + globs = ['*', 'id']; + filtered = notate(data).filter(globs).value; + expect(filtered.name).toEqual(data.name); + expect(filtered.id).toEqual(data.id); + }); + + test('#filter() » wildcards', () => { + const data = { + x: { + y: { z: 1 }, + a: { b: 2 } + }, + c: 3, + d: { + e: { f: 4, g: { i: 5 } } + } + }; + + const filter = (globs: string[]): Notation => notate(structuredClone(data)).filter(globs); + let result: UnknownObject; + + const check1 = (): void => { + expect(result.x.y.z).toEqual(1); + expect(result.x.a.b).toEqual(2); + expect(result.c).toBeUndefined(); + expect(result.d).toBeUndefined(); + }; + + // these should be treated the same: + // 'x.*.*' === 'x.*' === 'x' + + result = filter(['x.*.*']).value; + check1(); + result = filter(['x.*']).value; + check1(); + result = filter(['x']).value; + check1(); + + // these should NOT be treated the same: + // '!x.*.*' !== '!x.*' !== '!x' + + result = filter(['*', '!x.*.*']).value; + // console.log(result); + expect(utils.type(result.x)).toEqual('object'); + expect(utils.type(result.x.y)).toEqual('object'); + expect(utils.type(result.x.a)).toEqual('object'); + expect(Object.keys(result.x.y).length).toEqual(0); + expect(Object.keys(result.x.a).length).toEqual(0); + expect(result.c).toEqual(3); + expect(utils.type(result.d)).toEqual('object'); + + result = filter(['*', '!x.*']).value; + expect(utils.type(result.x)).toEqual('object'); + expect(Object.keys(result.x).length).toEqual(0); + expect(result.c).toEqual(3); + expect(utils.type(result.d)).toEqual('object'); + + result = filter(['*', '!x']).value; + // console.log('!x\t', result); + expect(result.x).toBeUndefined(); + expect(result.c).toEqual(3); + expect(utils.type(result.d)).toEqual('object'); + + result = filter(['*']).value; + // expect(JSON.stringify(result)).toEqual(JSON.stringify(data)); + expect(result).toEqual(data); + // console.log('*\t', result); + + result = filter(['*', '!*']).value; + expect(result).toEqual({}); + + // console.log(JSON.stringify(data, null, ' ')); + + result = filter(['*', '!*.*.*']).value; + // console.log('!*.*.*\n', JSON.stringify(result, null, ' ')); + expect(utils.type(result.x)).toEqual('object'); + expect(utils.type(result.x.y)).toEqual('object'); + expect(Object.keys(result.x.y).length).toEqual(0); + expect(utils.type(result.x.a)).toEqual('object'); + expect(Object.keys(result.x.a).length).toEqual(0); + expect(utils.type(result.d.e)).toEqual('object'); + expect(Object.keys(result.d.e).length).toEqual(0); + expect(result.c).toEqual(3); + + result = filter(['*', '!*.*']).value; + // console.log('!*.*\n', JSON.stringify(result, null, ' ')); + expect(utils.type(result.x)).toEqual('object'); + expect(Object.keys(result.x).length).toEqual(0); + expect(utils.type(result.d)).toEqual('object'); + expect(Object.keys(result.d).length).toEqual(0); + expect(result.c).toEqual(3); + }); + + test('#filter() » other', () => { + const globs = [ + '*', + '!box', + 'box.model.*', + '!bValid.*', + '!sto.p2m', + '!sto.contact.*', + '!sto.partners.*', + '!sto.powOp.*' + ]; + const data = { + id: 'TR001', + box: { + model: { code: 'N22', description: 'Normal 22 kVA' }, + supplier: 'EFA', + router: { brandModel: 'TELT 104' }, + connection: { operator: 'VODA', method: '3G' } + }, + bValid: { + method: 'OP', + comment: '', + date: 'Mon Nov 18 2013 15:41:43 GMT+0200 (EET)' + } + }; + const originalClone = utils.cloneDeep(data); + + const filtered = notate(data).filter(globs).value; + expect(filtered.box.model).toBeDefined(); + expect(filtered.box.router).toBeUndefined(); // "!box" + expect(filtered.bValid).toEqual({}); // "!validation.*" + expect(data).toEqual(originalClone); + }); + + test('#filter() » bracket', () => { + expect(notate([{ x: 1 }]).filter(['*']).value).toEqual([{ x: 1 }]); + expect(notate([{ x: 1 }]).filter(['[*]']).value).toEqual([{ x: 1 }]); + + const obj = [{ x: 1, y: [2, 3, { z: 4 }] }, [1, 2, 3], { 'my-prop': [4, 5] }]; + + // [0].x.* is invalid since x is not an object but a number + expect(() => notate(obj).filter(['[*]', '![0].x.*'])).toThrow(); + + const globs = ['[*]', '![0].x', '![0].y[1]', '![0].y[2].z', '![2]["my-prop"][0]']; + const filtered = notate(obj).filter(globs).value; + + expect(filtered[0].y[1]).toEqual({ z: 4 }); + expect(filtered[0].x).toBeUndefined(); + expect(filtered[1]).toEqual([1, 2, 3]); + expect(filtered[2]['my-prop']).toEqual([5]); + }); + + test('#filter() » bracket 2', () => { + let filtered: UnknownObject; + + const obj = [ + [1, 2, 3], + [4, 5, [[6]]], + [7, 8] + ]; + + let globs = ['[*]', '![*][1]', '![0][1]', '![0][2]', '![1][2][0]']; + filtered = notate(obj).filter(globs).value; + expect(filtered[0].length).toEqual(1); + + filtered = notate([0, 1, 2]).filter(['[*]', '![2]', '![0]']).value; + expect(filtered).toEqual([1]); + + filtered = notate([0, 1, [2, 3], 4, 5]).filter(['[*]', '![2][*]']).value; + expect(filtered).toEqual([0, 1, [], 4, 5]); + + filtered = notate([0, 1, [2, 3, 4], 5, 6]).filter(['[*]', '![2][1]']).value; + expect(filtered).toEqual([0, 1, [2, 4], 5, 6]); + + filtered = notate(filtered).filter(['[*]', '![1]', '![4]']).value; + expect(filtered).toEqual([0, [2, 4], 5]); + + filtered = notate([0, 1, [2, 3, 4], 5, [6, 7, 8], 9]).filter(['[*]', '![*][1]']).value; + expect(filtered).toEqual([0, 1, [2, 4], 5, [6, 8], 9]); + + filtered = notate([0, 1, [2, 3, 4, 1], 5, [3, 6, 7, 8], 9]).filter(['[*]', '![*][*]']).value; + expect(filtered).toEqual([0, 1, [], 5, [], 9]); + + filtered = notate([0, 1, [2, 3, 4], 5, [{ x: 1 }], [6, 7], [8, [9, 10]]]).filter([ + '[*]', + '![*][*]' + ]).value; + expect(filtered).toEqual([0, 1, [], 5, [], [], []]); + + filtered = notate([0, 1, [2, [3]], 4]).filter(['[*]', '![2][1][*]']).value; + expect(filtered).toEqual([0, 1, [2, []], 4]); + + filtered = notate([0, 1, [2, [3, [5], 6]], 4]).filter(['[*]', '![2][*][1][*]']).value; + expect(filtered).toEqual([0, 1, [2, [3, [], 6]], 4]); + + filtered = notate([0, 1, [2, [[null], [5], 6]], [4]]).filter(['[*]', '![2][*][*][*]']).value; + expect(filtered).toEqual([0, 1, [2, [[], [], 6]], [4]]); + + filtered = notate([0, 1, [2], [null], null, [undefined], undefined]).filter([ + '[*]', + '![*][*]' + ]).value; + expect(filtered).toEqual([0, 1, [], [], null, [], undefined]); + + filtered = notate([{ x: [3] }]).filter(['[*]', '![0].x[*]']).value; + expect(filtered).toEqual([{ x: [] }]); + + filtered = notate([{ x: [{ y: 1 }, { z: 2, y: 3 }] }]).filter(['[*]', '![0].x[*].y']).value; + expect(filtered).toEqual([{ x: [{}, { z: 2 }] }]); + + const obj2 = [{ x: [{ y: 1 }, { z: 2, y: 3 }] }]; + globs = ['[*]', '![0].x[*].y.*']; + // expect no change bec. no y is object + expect(notate(obj2).filter(globs).value).toEqual(obj2); + // same should throw in strict mode + filtered = (): UnknownObject => notate(obj2, { strict: true }).filter(globs).value; + expect(filtered).toThrow(); + + const obj3 = [{ x: [{ y: { a: 1 } }, { z: 2, y: { b: 3, c: 4 } }] }]; + filtered = notate(obj3, { strict: true }).filter(globs).value; + expect(filtered).toEqual([{ x: [{ y: {} }, { z: 2, y: {} }] }]); + + const obj4 = [{ x: [{ y: [1] }, { z: 2, y: [3, 4] }] }]; + filtered = notate(obj4).filter(globs).value; + // expect no change bec. no y is object + expect(filtered).toEqual(obj4); + + const obj5 = [{ x: [{ y: [1] }, { z: 2, y: [3, 4] }] }]; + filtered = notate(obj5).clone().filter(globs).value; + // expect no change bec. no y is object + expect(filtered).toEqual(obj5); + + globs = ['[*]', '![0].x[*].y[*]']; + filtered = notate(obj5).clone().filter(globs).value; + expect(filtered).toEqual([{ x: [{ y: [] }, { z: 2, y: [] }] }]); + + filtered = notate({ x: null }).filter(['*', '!x[*]']).value; + expect(filtered).toEqual({ x: [] }); + filtered = notate({ x: undefined }).filter(['*', '!x[*]']).value; + expect(filtered).toEqual({ x: [] }); + + // the glob wildcard determines the empty value ({} or []) in non-strict + // mode + filtered = notate({ x: null }).filter(['*', '!x.*']).value; + expect(filtered).toEqual({ x: {} }); + filtered = notate({ x: undefined }).filter(['*', '!x.*']).value; + expect(filtered).toEqual({ x: {} }); + + // should throw for null & undefined in strict mode + filtered = (): UnknownObject => + notate({ x: null }, { strict: true }).filter(['*', '!x[*]']).value; + expect(filtered).toThrow(); + filtered = (): UnknownObject => + notate({ x: undefined }, { strict: true }).filter(['*', '!x[*]']).value; + expect(filtered).toThrow(); + filtered = (): UnknownObject => + notate({ x: null }, { strict: true }).filter(['*', '!x.*']).value; + expect(filtered).toThrow(); + filtered = (): UnknownObject => + notate({ x: undefined }, { strict: true }).filter(['*', '!x.*']).value; + expect(filtered).toThrow(); + + // should always throw on critical type-mismatch (regardless of strict mode) + filtered = (): UnknownObject => notate({ x: 'string' }).filter(['*', '!x.*']).value; + expect(filtered).toThrow(); + filtered = (): UnknownObject => notate({ x: true }).filter(['*', '!x[*]']).value; + expect(filtered).toThrow(); + }); + + test('#filter() » bracket 3', () => { + const obj = { + a: { x: 1, y: 2 }, + b: { x: 3, y: 4 } + }; + let filtered; + + const expected = { a: { x: 1 }, b: { x: 3 } }; + filtered = notate(obj).filter('*.x').value; + expect(filtered).toEqual(expected); + filtered = notate(obj).filter('*["x"]').value; + expect(filtered).toEqual(expected); + filtered = notate(obj).filter("*['x']").value; + expect(filtered).toEqual(expected); + + const obj2 = { 'x.y': { z: 1 }, x: { y: { z: 2 } } }; + filtered = notate(obj2).filter('["x.y"].z').value; + expect(filtered).toEqual({ 'x.y': { z: 1 } }); + const nota = notate(obj2); + expect(nota.filter('x.y.z').value).toEqual({ x: { y: { z: 2 } } }); + expect(nota.filter('x.y.z').value).toEqual(nota.filter('x').value); + + const obj3 = { a: [1], b: 2, c: [3, 4], 'd.e': 5 }; + filtered = notate(obj3).filter(['c[1]', '["d.e"]', 'a[*]']).value; + // c[1] will create sparse array which is normal + expect(filtered).toEqual({ a: [1], c: [undefined, 4], 'd.e': 5 }); + }); +}); diff --git a/test/utils.spec.ts b/test/utils.spec.ts new file mode 100644 index 0000000..6fea8ab --- /dev/null +++ b/test/utils.spec.ts @@ -0,0 +1,204 @@ +/* eslint camelcase:0, consistent-return:0, max-lines-per-function:0 */ + +import type { UnknownObject } from '../src/types.js'; +import { utils } from '../src/utils.js'; + +describe('utils', () => { + test('.type(), .ensureArray()', () => { + expect(utils.type({})).toEqual('object'); + expect(utils.type([])).toEqual('array'); + expect(utils.type(null)).toEqual('null'); + expect(utils.type(undefined)).toEqual('undefined'); + expect(utils.type(new Error())).toEqual('error'); + expect(utils.type(new Date())).toEqual('date'); + + expect(utils.ensureArray(null)).toEqual([]); + expect(utils.ensureArray(undefined)).toEqual([]); + expect(utils.ensureArray(true)).toEqual([true]); + expect(utils.ensureArray(false)).toEqual([false]); + expect(utils.ensureArray([null])).toEqual([null]); + expect(utils.ensureArray(1)).toEqual([1]); + expect(utils.ensureArray('str')).toEqual(['str']); + }); + + test('.hasOwn(), .cloneDeep()', () => { + expect(utils.hasOwn({ a: 1 }, 'a')).toEqual(true); + expect(utils.hasOwn({ a: 1 }, 'b')).toEqual(false); + expect(utils.hasOwn({}, 'hasOwnProperty')).toEqual(false); + expect(utils.hasOwn({ hasOwnProperty: () => true }, 'x')).toEqual(false); + function Obj() {} // eslint-disable-line + Obj.prototype.hasOwnProperty = (): boolean => true; + expect(utils.hasOwn(new Obj(), 'x')).toEqual(false); + expect(utils.hasOwn(['0', 'a'], 0)).toEqual(true); + expect(utils.hasOwn(['0', 'a'], '0')).toEqual(false); + expect(utils.hasOwn(['2', 'a'], 2)).toEqual(false); + + expect(utils.cloneDeep({})).toEqual({}); + // @ts-expect-error: test null + expect(utils.cloneDeep(null)).toEqual(null); + const o = { a: { b: { c: [1, { o: 2 }, 3] }, x: true, y: { d: 'e', f: 4 } }, z: 5 }; + const copy = utils.cloneDeep(o); + expect(copy).toEqual(o); + expect(copy === o).toEqual(false); + const copy2 = utils.cloneDeep([o]); + expect(copy2).toEqual([o]); + }); + + test('.each(), .eachRight()', () => { + const a = [1, 2, 3, 4]; + + let out: number[] = []; + utils.each(a, (value, index, list) => { + expect(value).toEqual(a[index]); + expect(index).toEqual(a[index] - 1); + expect(a).toEqual(list); + out.push(value); + }); + expect(out).toEqual(a); + + out = []; + utils.eachRight(a, (value, index, list) => { + expect(value).toEqual(a[index]); + expect(index).toEqual(a[index] - 1); + expect(a).toEqual(list); + out.push(value); + }); + expect(out).toEqual(a.reverse()); + + // break out / return early test + out = []; + utils.each(a, (value, index): void | false => { + if (index <= 1) { + out.push(value); + } else { + return false; + } + }); + expect(out.length).toEqual(2); + + out = []; + utils.eachRight(a, (value, index): void | false => { + if (index > 1) { + out.push(value); + } else { + return false; + } + }); + expect(out.length).toEqual(2); + }); + + test('.eachItem()', () => { + const c1 = [1, 'a', true, { x: [2] }, [3]]; + let out: unknown[] = []; + utils.eachItem(c1, (item: unknown, index: number, collection: unknown[]) => { + out.push([index, item]); + expect(collection).toEqual(c1); + }); + expect(out).toEqual([ + [0, 1], + [1, 'a'], + [2, true], + [3, { x: [2] }], + [4, [3]] + ]); + + const c2 = { a: 1, b: true, c: [2], d: { e: 3 }, f: 'f' }; + out = []; + utils.eachItem(c2, (item: unknown, key: string, collection: UnknownObject) => { + out.push([key, item]); + expect(collection).toEqual(c2); + }); + expect(out).toEqual([ + ['a', 1], + ['b', true], + ['c', [2]], + ['d', { e: 3 }], + ['f', 'f'] + ]); + }); + + test('.stringOrArrayOf(), .hasSingleItemOf()', () => { + expect(utils.stringOrArrayOf('test', 'test')).toEqual(true); + expect(utils.stringOrArrayOf(['test'], 'test')).toEqual(true); + expect(utils.stringOrArrayOf(['test'], 'x')).toEqual(false); + // @ts-expect-error: testing args + expect(utils.stringOrArrayOf([1], 1)).toEqual(false); // should be string + + expect(utils.hasSingleItemOf(['test'], 'test')).toEqual(true); + expect(utils.hasSingleItemOf(['t'])).toEqual(true); + expect(utils.hasSingleItemOf(['t'], 't')).toEqual(true); + expect(utils.hasSingleItemOf(['t'], 'x')).toEqual(false); + // @ts-expect-error: testing args + expect(utils.hasSingleItemOf([1], 1)).toEqual(true); + }); + + test('.pregQuote()', () => { + expect(utils.pregQuote('*')).toEqual('\\*'); + expect(utils.pregQuote('[.+]')).toEqual('\\[\\.\\+\\]'); + expect(utils.pregQuote('[a-z]')).toEqual('\\[a\\-z\\]'); + expect(utils.pregQuote('(?:1|2)')).toEqual('\\(\\?\\:1\\|2\\)'); + expect(utils.pregQuote('x y z 1 2 3')).toEqual('x y z 1 2 3'); + }); + + test('.normalizeNote()', () => { + expect(utils.normalizeNote('a')).toEqual('a'); + expect(() => utils.normalizeNote('a.b')).toThrow(); + expect(() => utils.normalizeNote('[a.b]')).toThrow(); + expect(() => utils.normalizeNote('["a.b"]')).not.toThrow(); + + expect(utils.normalizeNote('[1]')).toEqual(1); + expect(() => utils.normalizeNote('[1.1]')).toThrow(); + expect(() => utils.normalizeNote('[-1]')).toThrow(); + + expect(utils.normalizeNote('["-1"]')).toEqual('-1'); + expect(utils.normalizeNote('["1"]')).toEqual('1'); + expect(utils.normalizeNote('["[x]"]')).toEqual('[x]'); + expect(utils.normalizeNote('["x.y"]')).toEqual('x.y'); + + expect(() => utils.normalizeNote('[]')).toThrow(); + // obj[''] = value is allowed in JS + expect(() => utils.normalizeNote('[""]')).not.toThrow(); + // but cannot be represented without brackets + expect(() => utils.normalizeNote('')).toThrow(); + }); + + test('.removeTrailingWildcards()', () => { + expect(utils.removeTrailingWildcards('*[*]')).toEqual('*'); + expect(utils.removeTrailingWildcards('[*].*')).toEqual('[*]'); + expect(utils.removeTrailingWildcards('*[*].*[*]')).toEqual('*'); + expect(utils.removeTrailingWildcards('*[*].*[*].*')).toEqual('*'); + expect(utils.removeTrailingWildcards('!*[*].*[*].*')).toEqual('!*[*].*[*].*'); + expect(utils.removeTrailingWildcards('*[*].*[*].*.x')).toEqual('*[*].*[*].*.x'); + expect(utils.removeTrailingWildcards('x.*[*].*[*].*')).toEqual('x'); + expect(utils.removeTrailingWildcards('[*].*[*].*')).toEqual('[*]'); + expect(utils.removeTrailingWildcards('[*].*[*].*[*]')).toEqual('[*]'); + expect(utils.removeTrailingWildcards('![*].*[*].*[*]')).toEqual('![*].*[*].*[*]'); + expect(utils.removeTrailingWildcards('[*].*[*].*[*].x')).toEqual('[*].*[*].*[*].x'); + expect(utils.removeTrailingWildcards('x[*].*[*].*[*]')).toEqual('x'); + }); + + test('.cloneDeep()', () => { + const now = Date.now(); + const original: UnknownObject = { + str: 'string', + num: 1, + bool: true, + date: new Date(now), + regexp: /abc/i, + arr: [1, { a: 2, b: 3 }, [4, 5]], + obj: { x: 1, y: { z: true } }, + nil: null, + undef: undefined + }; + let cloned = utils.cloneDeep(original); + expect(original).toEqual(cloned); + + // symbols are unique, so won't be exact but values should match + original.symbol = Symbol('test'); + cloned = utils.cloneDeep(original); + expect(original.symbol.valueOf()).toEqual(cloned.symbol.valueOf()); + + original.circular = original; + expect(() => utils.cloneDeep(original)).toThrow(); + }); +}); diff --git a/test/utils.targeted.spec.ts b/test/utils.targeted.spec.ts new file mode 100644 index 0000000..b9331db --- /dev/null +++ b/test/utils.targeted.spec.ts @@ -0,0 +1,86 @@ +/* Behavior-asserting tests targeting mutation-test survivors in utils. */ + +import { utils } from '../src/utils.js'; + +describe('utils (mutation-targeted)', () => { + test('cloneDeep() clones Date / RegExp / Symbol exactly', () => { + const d = new Date('2020-01-02T03:04:05.678Z'); + const cd = (utils.cloneDeep({ d }) as { d: Date }).d; + expect(cd).toBeInstanceOf(Date); + expect(cd).not.toBe(d); + expect(cd.getTime()).toEqual(d.getTime()); + + const re = /ab+c/gi; + re.lastIndex = 2; + const cre = (utils.cloneDeep({ re }) as { re: RegExp }).re; + expect(cre).toBeInstanceOf(RegExp); + expect(cre).not.toBe(re); + expect(cre.source).toEqual('ab+c'); + expect(cre.flags).toEqual('gi'); + expect(cre.lastIndex).toEqual(2); + + const s = Symbol('x'); + const cs = (utils.cloneDeep({ s }) as { s: symbol }).s; + expect(typeof cs).toEqual('object'); // Object-wrapped symbol, not the raw symbol + expect((cs as { valueOf(): symbol }).valueOf()).toBe(s); + }); + + test('eachRight() iterates in reverse and stops on `false`', () => { + const seen: number[] = []; + utils.eachRight([1, 2, 3, 4], (v) => { + seen.push(v as number); + if (v === 3) return false; + }); + expect(seen).toEqual([4, 3]); // reverse order, stopped after 3 + }); + + test('hasOwn() distinguishes objects, arrays and bad keys', () => { + expect(utils.hasOwn({ a: 1 }, 'a')).toEqual(true); + expect(utils.hasOwn({ a: 1 }, 'b')).toEqual(false); + expect(utils.hasOwn({ a: 1 }, '')).toEqual(false); // empty key + expect(utils.hasOwn(['x', 'y'], 0)).toEqual(true); + expect(utils.hasOwn(['x', 'y'], 2)).toEqual(false); // out of range + expect(utils.hasOwn(['x', 'y'], -1)).toEqual(false); // negative + expect(utils.hasOwn(['x', 'y'], '0')).toEqual(false); // string index on array + }); + + test('stringOrArrayOf()', () => { + expect(utils.stringOrArrayOf('a', 'a')).toEqual(true); + expect(utils.stringOrArrayOf(['a'], 'a')).toEqual(true); + expect(utils.stringOrArrayOf(['a'], 'b')).toEqual(false); + expect(utils.stringOrArrayOf(['a', 'b'], 'a')).toEqual(false); // length must be 1 + expect(utils.stringOrArrayOf([], 'a')).toEqual(false); + expect(utils.stringOrArrayOf('a', 'b')).toEqual(false); + }); + + test('hasSingleItemOf()', () => { + expect(utils.hasSingleItemOf(['a'], 'a')).toEqual(true); + expect(utils.hasSingleItemOf(['a'], 'b')).toEqual(false); + expect(utils.hasSingleItemOf(['a', 'b'], 'a')).toEqual(false); // length must be 1 + expect(utils.hasSingleItemOf(['a'])).toEqual(true); // no rest -> only length check + expect(utils.hasSingleItemOf(['a'], 'a', 'b')).toEqual(true); // rest>1 -> length only + }); + + test('joinNotes() uses dots and brackets correctly', () => { + expect(utils.joinNotes(['a', 'b', 'c'])).toEqual('a.b.c'); + expect(utils.joinNotes(['x', 'y', '[2]', 'z'])).toEqual('x.y[2].z'); + expect(utils.joinNotes(['a', '[0]'])).toEqual('a[0]'); + expect(utils.joinNotes(['only'])).toEqual('only'); + }); + + test('normalizeNote() handles single/double/backtick brackets', () => { + expect(utils.normalizeNote("['a']")).toEqual('a'); // m[1] + expect(utils.normalizeNote('["b"]')).toEqual('b'); // m[2] + expect(utils.normalizeNote('[`c`]')).toEqual('c'); // m[3] + expect(utils.normalizeNote('[7]')).toEqual(7); // numeric index + expect(() => utils.normalizeNote('bad note')).toThrow(/Invalid note/); + }); + + test('getNewNotation() trims, falls back, or throws', () => { + expect(utils.getNewNotation(' new ', 'old')).toEqual('new'); // trimmed + expect(utils.getNewNotation(null, 'old')).toEqual('old'); // fallback to notation + expect(utils.getNewNotation(undefined, 'old')).toEqual('old'); + expect(() => utils.getNewNotation(' ', 'old')).toThrow(/Invalid new notation/); + expect(() => utils.getNewNotation(null, null)).toThrow(/Invalid new notation/); + }); +}); From 2b0815c4a759a0f91ce85136cef36694aea83589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20Y=C4=B1ld=C4=B1r=C4=B1m?= Date: Mon, 15 Jun 2026 20:20:33 +0300 Subject: [PATCH 08/17] created stryker config for mutation testing --- stryker.config.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 stryker.config.json diff --git a/stryker.config.json b/stryker.config.json new file mode 100644 index 0000000..131fbd8 --- /dev/null +++ b/stryker.config.json @@ -0,0 +1,16 @@ +{ + "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json", + "packageManager": "npm", + "testRunner": "vitest", + "reporters": ["html", "json", "clear-text", "progress"], + "htmlReporter": { "fileName": "test/stryker/mutation-report.html" }, + "jsonReporter": { "fileName": "test/stryker/mutation-report.json" }, + "coverageAnalysis": "perTest", + "mutate": [ + "src/**/*.ts", + "!src/**/index.ts", + "!src/types.ts", + "!src/core/I*.ts" + ], + "thresholds": { "high": 90, "low": 70, "break": 80 } +} From 939045becb71b2ce76f4a49e88f7f34fcd651989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20Y=C4=B1ld=C4=B1r=C4=B1m?= Date: Mon, 15 Jun 2026 20:20:47 +0300 Subject: [PATCH 09/17] Update LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index f083638..b4df5f2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2020, Onur Yıldırım . All rights reserved. +Copyright (c) 2021, Onur Yıldırım . All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From cdca7ee33276b7d6a31ff08b33e0a7e5be1c7812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20Y=C4=B1ld=C4=B1r=C4=B1m?= Date: Mon, 15 Jun 2026 20:20:59 +0300 Subject: [PATCH 10/17] v3.0.0 --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c6d585..a4cf8f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,37 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org). +## 3.0.0 (2024-07-04) + +### Changed +- **Breaking**: ESM now. +- **Breaking**: Requires Node v20 and above. +- **Breaking**: `Notation.Error` is removed, use `NotationError` instead. +- **Breaking**: `Notation.Glob` is removed, use `NotationGlob` instead. +- **Breaking**: `NotationGlob#levels` is removed, use `NotationGlob#notes` instead. +- **Breaking**: `Notation#eachLevel()` is removed, use `Notation#eachNote()` instead. +- **Breaking**: `Notation#aggregate()` is removed, use `Notation#expand()` instead. +- **Breaking**: `Notation#delete()` is removed, use `Notation#remove()` instead. +- **Breaking**: `Notation#renote()` is removed, use `Notation#rename()` instead. +- **Breaking**: `Notation#copyToNew()` is removed, use `Notation#extract()` instead. +- **Breaking**: `Notation#moveToNew()` is removed, use `Notation#extrude()` instead. +- **Breaking**: `Notation.countLevels()` is removed, use `Notation.countNotes()` instead. +- **Breaking**: Now ships an `exports` map; only the package root (`notation`) and `package.json` are importable (no deep imports into `lib/`). +- Re-written in TypeScript. +- Modernized the toolchain: **TypeScript 6**, ESM-only build via `tsc` (no bundler). +- Switched testing + coverage to **Vitest** (from Jest/ts-jest + c8); coverage uses the istanbul provider. +- Switched linting + formatting to **Biome** (from ESLint). +- Switched CI to **GitHub Actions** (from Travis). +- Dropped dev dependencies no longer needed: `jest`, `ts-jest`, `c8`, `eslint`, `lodash`, `pkgroll`. +- Shared config via `tsconfig-oy` and `biome-config-oy`. + +### Fixed +- An issue with `Notation#eachValue()` method where bracket notation was ignored. +- An issue with `Notation#set()` method where we get i.e. `TypeError: Cannot set x of undefined` when working with sparse arrays. +- An issue with `Notation#filter()` method where in some cases, value was set for the level notation instead of given notation. +- An issue with `Notation#normalize()` method where in some cases, intersections were ignored. + + ## 2.0.0 (2020-09-04) This is a big major release with lots of **improvements** and some **breaking changes**. Please read the changes below and re-run your application tests after you upgrade to v2. From 18579ad921be647c72a78bcd557af4b6fd1fb268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20Y=C4=B1ld=C4=B1r=C4=B1m?= Date: Mon, 15 Jun 2026 20:21:03 +0300 Subject: [PATCH 11/17] Update README.md --- README.md | 78 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index fbfb979..c1605ff 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,26 @@ # Notation.js -[![build-status](https://img.shields.io/travis/onury/notation.svg?branch=master&style=flat-square)](https://travis-ci.org/onury/notation) -[![coverage-status](https://img.shields.io/coveralls/github/onury/notation/master.svg?&style=flat-square)](https://coveralls.io/github/onury/notation?branch=master) -[![npm](http://img.shields.io/npm/v/notation.svg?style=flat-square)](https://www.npmjs.com/package/notation) -[![release](https://img.shields.io/github/release/onury/notation.svg?style=flat-square)](https://github.com/onury/notation) -[![vulnerabilities](https://snyk.io/test/github/onury/notation/badge.svg?style=flat-square)](https://snyk.io/test/github/onury/notation) -[![license](http://img.shields.io/npm/l/notation.svg?style=flat-square)](https://github.com/onury/notation/blob/master/LICENSE) -[![documentation](https://img.shields.io/badge/docs-click_to_read-c27cf4.svg?docs=click_to_read&style=flat-square)](https://onury.io/notation/api) +

+ build + coverage + mutation score + version + ESM + TS + license +

-> © 2021, Onur Yıldırım ([@onury](https://github.com/onury)). MIT License. +**Important**: This module is **ESM** 🔆. Please [**read this**](https://gist.github.com/onury/d3f3d765d7db2e8b2d050d14315f2ac7). -Utility for modifying / processing the contents of JavaScript objects and arrays, via object or bracket notation strings or globs. (Node and Browser) +> © 2026, Onur Yıldırım ([@onury](https://github.com/onury)). MIT License. + +A utility for reading, modifying, and filtering the contents of JavaScript objects and arrays — using object/bracket **notation** strings or **glob** patterns. ```js Notation.create({ x: 1 }).set('some.prop', true).filter(['*.prop']).value // { some: { prop: true } } ``` -> _Note that this library should be used to manipulate **data objects** with enumerable properties. It will NOT deal with preserving the prototype-chain of the given object or objects with circular references._ +> _This library is intended for **data objects** with enumerable properties. It does **not** preserve an object's prototype chain, and does not support objects with circular references._ ## Table of Contents - [Usage](#usage) @@ -26,7 +30,8 @@ Notation.create({ x: 1 }).set('some.prop', true).filter(['*.prop']).value // { s - [Object and Bracket Notation Syntax](#object-and-bracket-notation-syntax) - [Globs and Data Integrity](#globs-and-data-integrity) - [Source Object Mutation](#source-object-mutation) -- [API Reference][docs] +- [API Reference](#documentation) +- [Quality](#quality) ## Usage @@ -35,21 +40,11 @@ Install via **NPM**: ```sh npm i notation ``` -In Node/CommonJS environments: -```js -const { Notation } = require('notation'); -``` -With transpilers (TypeScript, Babel): + ```js import { Notation } from 'notation'; ``` -In (Modern) Browsers: -```html - - -``` + ## Notation `Notation` is a class for modifying or inspecting the contents (property keys and values) of a data object or array. @@ -64,7 +59,7 @@ if (obj return obj.very.deep.prop === undefined ? defaultValue : obj.very.deep.prop; } ``` -With `Notation`, you could do this: +With `Notation`, you can do this: ```js const notate = Notation.create; return notate(obj).get('very.deep.prop', defaultValue); @@ -105,16 +100,16 @@ See [API Reference][docs] for more... With a glob-notation, you can use wildcard stars `*` and bang `!` prefix. A wildcard star will include all the properties at that level and a bang prefix negates that notation for exclusion. - Only **`Notation#filter()`** method accepts glob notations. Regular notations (without any wildcard `*` or `!` prefix) should be used with all other members of the **`Notation`** class. -- For raw Glob operations, you can use the **`Notation.Glob`** class. +- For raw Glob operations, you can use the **`NotationGlob`** class. ### Normalizing a glob notation list Removes duplicates, redundant items and logically sorts the array: ```js -const { Notation } = require('notation'); +import { NotationGlob } from 'notation'; const globs = ['*', '!id', 'name', 'car.model', '!car.*', 'id', 'name', 'age']; -console.log(Notation.Glob.normalize(globs)); +console.log(NotationGlob.normalize(globs)); // ——» ['*', '!car.*', '!id', 'car.model'] ``` @@ -124,12 +119,12 @@ In the normalized result `['*', '!car.*', '!id', 'car.model']`: - (In non-restrictive mode) `car.model` is kept (although `*` matches it) bec. it's explicitly defined while we have a negated glob that also matches it: `!car.*`. ```js -console.log(Notation.Glob.normalize(globs, { restrictive: true })); +console.log(NotationGlob.normalize(globs, { restrictive: true })); // ——» ['*', '!car.*', '!id'] ``` - In restrictive mode, negated removes every match. -> _**Note**: `Notation#filter()` and `Notation.Glob.union()` methods automtically pre-normalize the given glob list(s)._ +> _**Note**: `Notation#filter()` and `NotationGlob.union()` methods automatically pre-normalize the given glob list(s)._ ### Union of two glob notation lists @@ -137,7 +132,7 @@ Unites two glob arrays optimistically and sorts the result array logically: ```js const globsA = ['*', '!car.model', 'car.brand', '!*.age']; const globsB = ['car.model', 'user.age', 'user.name']; -const union = Notation.Glob.union(globsA, globsB); +const union = NotationGlob.union(globsA, globsB); console.log(union); // ——» ['*', '!*.age', 'user.age'] ``` @@ -298,11 +293,30 @@ console.log(cloned.newProp); // ——» true ## Documentation -You can read the full [**API reference** here][docs]. +Read the full [**API reference**][docs] (currently documents v2). For what changed in v3, see the [change log](#change-log). ## Change-Log -Read the [CHANGELOG][changelog] especially if you're migrating from version `1.x.x` to version `2.0.0` and above. +Read the [CHANGELOG][changelog]. + +## Quality + +- **100% test coverage** (statements, branches, functions, lines) — enforced via + Vitest thresholds. Run `npm run cover`. +- **~84% mutation score** via [StrykerJS](https://stryker-mutator.io/). Run + `npm run mutation`. + +[Mutation testing](https://stryker-mutator.io/docs/) goes beyond coverage: it +makes hundreds of small edits ("mutants") to the source — flipping `>` to `>=`, +`&&` to `||`, returning `undefined`, etc. — and checks that a test **fails** for +each. It catches tests that *execute* code without actually *asserting* its +behavior (the trap where `function getPositive(x){ return x }` reaches 100% +coverage but verifies nothing). Most of the surviving mutants here are +[equivalent mutants](https://stryker-mutator.io/docs/mutation-testing-elements/equivalent-mutants/) +in the glob normalization/cover/union logic — redundant-but-harmless branches +that produce identical results — plus environment-defensive guards. These can't +be killed by definition, so the realistic, healthy target is a high score, not +100%. ## License From ca4fd3109a0504fd42325663f6e9247e371b1291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20Y=C4=B1ld=C4=B1r=C4=B1m?= Date: Mon, 15 Jun 2026 20:21:21 +0300 Subject: [PATCH 12/17] v3.0.0 --- package.json | 108 ++++++++++++++++----------------------------------- 1 file changed, 33 insertions(+), 75 deletions(-) diff --git a/package.json b/package.json index 59206e0..e7bd8f9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "notation", - "version": "2.0.0", + "version": "3.0.0", "description": "Utility for modifying / processing the contents of Javascript objects or arrays via object notation strings or globs.", "repository": "onury/notation", "license": "MIT", @@ -8,71 +8,35 @@ "name": "Onur Yıldırım", "email": "onur@cutepilot.com" }, - "main": "lib/notation.min.js", + "type": "module", + "main": "./lib/index.js", + "module": "./lib/index.js", + "types": "./lib/index.d.ts", + "exports": { + ".": { + "types": "./lib/index.d.ts", + "import": "./lib/index.js", + "default": "./lib/index.js" + }, + "./package.json": "./package.json" + }, + "sideEffects": false, "files": [ "lib", "LICENSE" ], + "engines": { + "node": ">=20" + }, "scripts": { - "lint": "eslint ./src", - "snyk": "snyk test", - "prebuild": "npm run snyk && npm run lint", - "build:dev": "webpack --mode=development --env.WEBPACK_OUT=development --progress --colors", - "build:prod": "webpack --mode=production --env.WEBPACK_OUT=production --progress --colors", - "build": "npm run build:prod && npm run build:dev", - "watch": "webpack --env.WEBPACK_OUT=nomin --progress --colors --watch", + "lint": "biome check src test", + "format": "biome check --write src test", + "build": "tsc --project tsconfig.build.json", "pretest": "npm run lint", - "test": "jest --testPathPattern='test/.+$' --verbose --no-cache --runInBand", - "cover": "jest --testPathPattern='test/.+$' --verbose --no-cache --runInBand --coverage", - "docs": "docma", - "docs:serve": "docma && docma serve", - "release": "npm run build && npm test && npm run docs" - }, - "babel": { - "presets": [ - "@babel/preset-env" - ] - }, - "jest": { - "testEnvironment": "node", - "collectCoverageFrom": [ - "src/**/*.js", - "!src/index.js" - ], - "testRegex": "test/.+(test|spec)\\.js$", - "transform": { - "^.+\\.jsx?$": "babel-jest" - }, - "testPathIgnorePatterns": [ - "/backup/", - "/node_modules/", - "/test/tmp/", - "/test/coverage/", - "/test/config/", - "/test/data/", - "/test/helpers/", - "/lib/" - ], - "transformIgnorePatterns": [ - "/node_modules/", - "/test/(coverage|helpers|tmp|config|data)/" - ], - "coverageDirectory": "/test/coverage", - "coveragePathIgnorePatterns": [ - "/backup/", - "/coverage/", - "/node_modules/", - "/test/(helpers|tmp|config|data)/" - ], - "modulePathIgnorePatterns": [ - "/backup/", - "/coverage/", - "/test/helpers/", - "/test/tmp/", - "/test/config/", - "/test/data/", - "/lib/" - ] + "test": "vitest run", + "cover": "vitest run --coverage", + "mutation": "stryker run", + "prepublishOnly": "npm run build" }, "keywords": [ "object", @@ -92,19 +56,13 @@ "build" ], "devDependencies": { - "@babel/core": "^7.11.6", - "@babel/preset-env": "^7.11.5", - "babel-eslint": "^10.1.0", - "babel-jest": "^26.3.0", - "babel-loader": "^8.1.0", - "babel-plugin-istanbul": "^6.0.0", - "eslint": "^7.8.1", - "jest-cli": "^26.4.2", - "lodash": "^4.17.20", - "snyk": "^1.388.0", - "uglifyjs-webpack-plugin": "^2.2.0", - "webpack": "^4.44.1", - "webpack-cli": "^3.3.12" - }, - "dependencies": {} + "@biomejs/biome": "^2.5.0", + "@stryker-mutator/core": "^9.6.1", + "@stryker-mutator/vitest-runner": "^9.6.1", + "@types/node": "^25.9.3", + "@vitest/coverage-istanbul": "^4.1.8", + "tsconfig-oy": "^1.1.0", + "typescript": "^6.0.3", + "vitest": "^4.1.8" + } } From f47a231a8139ef7390dbd0c2dc2622832bcaf2f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20Y=C4=B1ld=C4=B1r=C4=B1m?= Date: Mon, 15 Jun 2026 20:22:09 +0300 Subject: [PATCH 13/17] updated core classes. breaking changes! --- src/core/Notation.ts | 1445 +++++++++++++++++++++++++++++++++++++ src/core/NotationError.ts | 47 ++ src/core/NotationGlob.ts | 1123 ++++++++++++++++++++++++++++ 3 files changed, 2615 insertions(+) create mode 100644 src/core/Notation.ts create mode 100644 src/core/NotationError.ts create mode 100644 src/core/NotationGlob.ts diff --git a/src/core/Notation.ts b/src/core/Notation.ts new file mode 100644 index 0000000..2eed536 --- /dev/null +++ b/src/core/Notation.ts @@ -0,0 +1,1445 @@ +/* eslint-disable max-lines */ + +import type { Collection, UnknownObject } from '../types.js'; +import { utils } from '../utils.js'; +import type { INotationFilterOptions } from './INotationFilterOptions.js'; +import type { INotationInspection } from './INotationInspection.js'; +import { DEFAULT_NOTATION_OPTIONS, type INotationOptions } from './INotationOptions.js'; +import { NotationError } from './NotationError.js'; +import { NotationGlob } from './NotationGlob.js'; + +const ERR = { + SOURCE: 'Invalid source. Expected a data object or array.', + DEST: 'Invalid destination. Expected a data object or array.', + NOTATION: 'Invalid notation: ', + NOTA_OBJ: 'Invalid notations object. ', + NO_INDEX: 'Implied index does not exist: ', + NO_PROP: 'Implied property does not exist: ' +}; + +// created test @ https://regex101.com/r/vLE16M/2 +const reMATCHER = /(\[(\d+|".*"|'.*'|`.*`)\]|[a-z$_][a-z$_\d]*)/gi; +// created test @ https://regex101.com/r/fL3PJt/1/ +// /^([a-z$_][a-z$_\d]*|\[(\d+|".*"|'.*'|`.*`)\])(\[(\d+|".*"|'.*'|`.*`)\]|(\.[a-z$_][a-z$_\d]*))*$/i +const reVALIDATOR = new RegExp( + '^(' + + '[a-z$_][a-z$_\\d]*' + // JS variable syntax + '|' + // OR + '\\[(\\d+|".*"|\'.*\')\\]' + // array index or object bracket notation + ')' + // exactly once + '(' + + '\\[(\\d+|".*"|\'.*\')\\]' + // followed by same + '|' + // OR + '\\.[a-z$_][a-z$_\\d]*' + // dot, then JS variable syntax + ')*' + // (both) may repeat any number of times + '$', + 'i' +); + +export type NotationEachCallback = ( + notation: string, + key: string, + value: unknown, + source: UnknownObject | unknown[] +) => void | false; + +export type NotationEachNoteCallback = ( + levelNotation: string, + note: string, + index: number, + noteList: string[] +) => void | false; + +export type NotationEachValueCallback = ( + levelValue: unknown, + levelNotation: string, + note: string, + index: number, + noteList: string[] +) => void | false; + +type NotationSource = T extends unknown[] ? unknown[] : UnknownObject; + +/** + * Notation.js for Node and Browser. + * + * Like in most programming languages, JavaScript makes use of dot-notation to + * access the value of a member of an object (or class). `Notation` class + * provides various methods for modifying / processing the contents of the + * given object; by parsing object notation strings or globs. + * + * Note that this class will only deal with enumerable properties of the source + * object; so it should be used to manipulate data objects. It will not deal + * with preserving the prototype-chain of the given object. + */ +export class Notation< + T extends Collection = UnknownObject, + S extends NotationSource = NotationSource +> { + private _source: S; + private _isArray: boolean; + private _options: INotationOptions = { ...DEFAULT_NOTATION_OPTIONS }; + + /** + * Initializes a new instance of `Notation`. + * + * @param [source={}] - The source object (or array) to be + * notated. Can either be an array or object. If omitted, defaults to an + * empty object. + * @param [options] - Notation options. + * + * @example + * const obj = { car: { brand: "Dodge", model: "Charger", year: 1970 } }; + * const notation = new Notation(obj); + * notation.get('car.model') // » "Charger" + * notation.remove('car.model').set('car.color', 'red').value + * // » { car: { brand: "Dodge", year: 1970, color: "red" } } + */ + constructor(source?: T, options?: INotationOptions) { + if (arguments.length === 0) { + this._source = {} as S; + } else if (utils.isCollection(source)) { + this._source = source as S; + } else { + throw new NotationError(ERR.SOURCE); + } + + this._isArray = Array.isArray(this._source); + this.options = options; + } + + // -------------------------------- + // INSTANCE PROPERTIES + // -------------------------------- + + /** + * Gets or sets notation options. + */ + get options(): INotationOptions { + return this._options; + } + set options(value: INotationOptions | undefined) { + this._options = { + ...DEFAULT_NOTATION_OPTIONS, + ...this._options, + ...value + }; + } + + /** + * Gets the value of the source object. + * + * @example + * const person = { name: "Onur" }; + * const me = Notation.create(person) + * .set("age", 36) + * .set("car.brand", "Ford") + * .set("car.model", "Mustang") + * .value; + * console.log(me); // { name: "Onur", age: 36, car: { brand: "Ford", model: "Mustang" } } + * console.log(person === me); // true + */ + get value(): S { + return this._source; + } + + // -------------------------------- + // INSTANCE METHODS + // -------------------------------- + + /** + * Recursively iterates through each key of the source object and invokes + * the given callback function with parameters, on each non-object value. + * + * @param callback - The callback function to be invoked on each on each + * non-object value. To break out of the loop, return `false` from within + * the callback. Callback signature: `callback(notation, key, value, + * object) { ... }` + * + * @returns {Notation} - The current `Notation` instance (self). + * + * @example + * const obj = { car: { brand: "Dodge", model: "Charger", year: 1970 } }; + * Notation.create(obj).each(function (notation, key, value, object) { + * console.log(notation, value); + * }); + * // "car.brand" "Dodge" + * // "car.model" "Charger" + * // "car.year" 1970 + */ + each(callback: NotationEachCallback): this { + _each(this._source, callback); + return this; + } + + /** + * Iterates through each note of the given notation string by evaluating it + * on the source object. + * + * @param notation - The notation string to be iterated through. + * @param callback - The callback function to be invoked on each iteration. + * To break out of the loop, return `false` from within the callback. + * Signature: `callback(levelValue, note, index, list)` + * + * @returns - The current `Notation` instance (self). + * + * @example + * const obj = { car: { brand: "Dodge", model: "Charger", year: 1970 } }; + * Notation.create(obj) + * .eachValue("car.brand", function (levelValue, note, index, list) { + * console.log(note, levelValue); // "car.brand" "Dodge" + * }); + */ + eachValue(notation: string, callback: NotationEachValueCallback): this { + let level = this._source; + Notation.eachNote(notation, (levelNotation, note, index, list): void | false => { + const nNote = utils.normalizeNote(note); + level = utils.hasOwn(level, nNote) ? level[nNote] : undefined; + if (callback(level, levelNotation, note, index, list) === false) return false; + }); + return this; + } + + /** + * Gets the list of notations from the source object (keys). + * + * @returns - An array of notation strings. + * + * @example + * const obj = { car: { brand: "Dodge", model: "Charger", year: 1970 } }; + * const notations = Notation.create(obj).getNotations(); + * console.log(notations); // [ "car.brand", "car.model", "car.year" ] + */ + getNotations(): string[] { + const list: string[] = []; + this.each((notation) => { + list.push(notation); + }); + return list; + } + + /** + * Deeply clones the source object. This is also useful if you want to + * prevent mutating the original source object. + * + *
+ * Note that `Notation` expects a data object (or array) with enumerable + * properties. In addition to plain objects and arrays; supported cloneable + * property/value types are primitives (such as `String`, `Number`, + * `Boolean`, `Symbol`, `null` and `undefined`) and built-in types (such as + * `Date` and `RegExp`). + * + * Enumerable properties with types other than these (such as methods, + * special objects, custom class instances, etc) will be copied by reference. + * Non-enumerable properties will not be cloned. + * + * If you still need full clone support, you can use a library like lodash. + * e.g. `Notation.create(_.cloneDeep(source))` + *
+ * + * @returns - The current `Notation` instance (self). + * + * @example + * const mutated = Notation.create(source1).set('newProp', true).value; + * console.log(source1.newProp); // ——» true + * + * const cloned = Notation.create(source2).clone().set('newProp', true).value; + * console.log('newProp' in source2); // ——» false + * console.log(cloned.newProp); // ——» true + */ + clone(): this { + this._source = utils.cloneDeep(this._source); + return this; + } + + /** + * Flattens the source object to a single-level object with notated keys. + * + * @returns - The current `Notation` instance (self). + * + * @example + * const obj = { car: { brand: "Dodge", model: "Charger", year: 1970 } }; + * console.log(Notation.create(obj).flatten().value); + * // { + * // "car.brand": "Dodge", + * // "car.model": "Charger", + * // "car.year": 1970 + * // } + */ + flatten(): this { + const o: UnknownObject = {}; + this.each((notation, key, value) => { + o[notation] = value; + }); + this._source = o; + return this; + } + + /** + * Aggregates notated keys of a (single-level) object, and nests them under + * their corresponding properties. This is the opposite of `Notation#flatten` + * method. This might be useful when expanding a flat object fetched from + * a database. + * + * @returns - The current `Notation` instance (self). + * + * @example + * const obj = { "car.brand": "Dodge", "car.model": "Charger", "car.year": 1970 } + * const expanded = Notation.create(obj).expand().value; + * console.log(expanded); // { car: { brand: "Dodge", model: "Charger", year: 1970 } }; + */ + expand(): this { + this._source = Notation.create({}).merge(this._source).value; + return this; + } + + /** + * Inspects the given notation on the source object by checking + * if the source object actually has the notated property; + * and getting its value if exists. + * + * @param notation - The notation string to be inspected. + * + * @returns - The inspection result object. + * + * @example + * Notation.create({ car: { year: 1970 } }).inspectGet("car.year"); + * // { has: true, value: 1970, lastNote: 'year', lastNoteNormalized: 'year' } + * Notation.create({ car: { year: 1970 } }).inspectGet("car.color"); + * // { has: false } + * Notation.create({ car: { color: undefined } }).inspectGet("car.color"); + * // { has: true, value: undefined, lastNote: 'color', lastNoteNormalized: 'color' } + * Notation.create({ car: { brands: ['Ford', 'Dodge'] } }).inspectGet("car.brands[1]"); + * // { has: true, value: 'Dodge', lastNote: '[1]', lastNoteNormalized: 1 } + */ + inspectGet(notation: string): INotationInspection { + let levelValue = this._source; + let result: INotationInspection = { + notation, + has: false, + value: undefined, + type: 'undefined', + level: -1, // just to comply with type + lastNote: '', + lastNoteNormalized: '', + parentIsArray: false + }; + let parent: UnknownObject | unknown[] | undefined; + + Notation.eachNote(notation, (levelNotation, note, index): void | false => { + const lastNoteNormalized = utils.normalizeNote(note); + + if (utils.hasOwn(levelValue, lastNoteNormalized)) { + const value = levelValue[lastNoteNormalized]; + // e.g. for note"[1]" of notation "b.c[1]", parent is an array (b.c) + // parent = Array.isArray(levelValue) ? levelValue : value; + parent = value; + levelValue = value; + result = { + ...result, + has: true, + value, + type: utils.type(value), + level: index + 1, + lastNote: note, + lastNoteNormalized + }; + } else { + result = { + ...result, + has: false, + value: undefined, + type: 'undefined', + level: index + 1, + lastNote: note, + lastNoteNormalized + }; + return false; // break out + } + }); + + if (parent === undefined || (result.has && parent === result.value)) { + parent = this._source; + } + result.parentIsArray = utils.type(parent) === 'array'; + + return result; + } + + /** + * Inspects and removes the given notation from the source object by + * checking if the source object actually has the notated property; and + * getting its value if exists, before removing the property. + * + * @param {string} notation - The notation string to be inspected. + * + * @returns - The inspection result object. + * + * @example + * const obj = { name: "John", car: { year: 1970 } }; + * let result = Notation.create(obj).inspectRemove("car.year"); + * // result » { notation: "car.year", has: true, value: 1970, lastNote: "year", lastNoteNormalized: "year" } + * // obj » { name: "John", car: {} } + * + * result = Notation.create({ car: { year: 1970 } }).inspectRemove("car.color"); + * // result » { notation: "car.color", has: false } + * Notation.create({ car: { color: undefined } }).inspectRemove("car['color']"); + * // { notation: "car.color", has: true, value: undefined, lastNote: "['color']", lastNoteNormalized: "color" } + * + * const obj = { car: { colors: ["black", "white"] } }; + * const result = Notation.create().inspectRemove("car.colors[0]"); + * // result » { notation: "car.colors[0]", has: true, value: "black", lastNote: "[0]", lastNoteNormalized: 0 } + * // obj » { car: { colors: [(empty), "white"] } } + */ + inspectRemove(notation: string): INotationInspection { + if (!notation) throw new Error(ERR.NOTATION + `'${notation}'`); + + const parentNotation = Notation.parent(notation); + const parent = parentNotation ? (this.get(parentNotation, null) as Collection) : this._source; + const parentIsArray = utils.type(parent) === 'array'; + const notes = Notation.split(notation); + const lastNote = notes[notes.length - 1]; + const lastNoteNormalized = utils.normalizeNote(lastNote); + + let result: INotationInspection; + let value: unknown; + + if (utils.hasOwn(parent, lastNoteNormalized)) { + value = parent[lastNoteNormalized]; + result = { + notation, + has: true, + value, + type: utils.type(value), + level: notes.length, + lastNote, + lastNoteNormalized, + parentIsArray + }; + + // if `preserveIndices` is enabled and this is an array, we'll + // splice the item out. otherwise, we'll use `delete` syntax to + // empty the item. + if (!this.options.preserveIndices && parentIsArray) { + parent.splice(lastNoteNormalized, 1); + } else { + delete parent[lastNoteNormalized]; + } + } else { + result = { + notation, + has: false, + value: undefined, + type: 'undefined', + level: notes.length, + lastNote, + lastNoteNormalized, + parentIsArray + }; + } + + return result; + } + + /** + * Checks whether the source object has the given notation as a (leveled) + * enumerable property. If the property exists but has a value of + * `undefined`, this will still return `true`. + * + * @param notation - The notation string to be checked. + * + * @example + * Notation.create({ car: { year: 1970 } }).has("car.year"); // true + * Notation.create({ car: { year: undefined } }).has("car.year"); // true + * Notation.create({}).has("car.color"); // false + */ + has(notation: string): boolean { + return this.inspectGet(notation).has; + } + + /** + * Checks whether the source object has the given notation as a (leveled) + * defined enumerable property. If the property exists but has a value of + * `undefined`, this will return `false`. + * + * @param notation - The notation string to be checked. + * + * @example + * Notation.create({ car: { year: 1970 } }).hasDefined("car.year"); // true + * Notation.create({ car: { year: undefined } }).hasDefined("car.year"); // false + * Notation.create({}).hasDefined("car.color"); // false + */ + hasDefined(notation: string): boolean { + return this.inspectGet(notation).value !== undefined; + } + + /** + * Gets the value of the corresponding property at the given notation. + * + * @param notation - The notation string to be processed. + * @param [defaultValue] - The default value to be returned if the property + * is not found or enumerable. + * + * @returns - The value of the notated property. + * + * @throws {NotationError} - If `strict` option is enabled, `defaultValue` + * is not set and notation does not exist. + * + * @example + * Notation.create({ car: { brand: "Dodge" } }).get("car.brand"); // "Dodge" + * Notation.create({ car: {} }).get("car.model", "Challenger"); // "Challenger" + * Notation.create({ car: { model: undefined } }).get("car.model", "Challenger"); // undefined + * + * @example get value when strict option is enabled // + * strict option defaults to false Notation.create({ car: {} + * }).get("car.model"); // undefined Notation.create({ car: {} }, { strict: + * false }).get("car.model"); // undefined // below will throw bec. strict + * = true, car.model does not exist // and no default value is given. + * Notation.create({ car: {} }, { strict: true }).get("car.model"); + */ + get(notation: string, defaultValue?: unknown): unknown { + const result = this.inspectGet(notation); + + // if strict and no default value is set, check if implied index or prop + // exists + if (this.options.strict && arguments.length < 2 && !result.has) { + const msg = result.parentIsArray ? ERR.NO_INDEX : ERR.NO_PROP; + throw new NotationError(msg + `'${notation}'`); + } + + return result.has ? result.value : defaultValue; + } + + /** + * Sets the value of the corresponding property at the given notation. If + * the property does not exist, it will be created and nested at the + * calculated level. If it exists; its value will be overwritten by + * default. + * + * @param notation - The notation string to be processed. + * @param value - The value to be set for the notated property. + * @param [mode="overwrite"] - Write mode. By default, this is set to + * `"overwrite"` which sets the value by overwriting the target object + * property or array item at index. To insert an array item (by shifting + * the index, instead of overwriting); set to `"insert"`. To prevent + * overwriting the value if exists, explicitly set to `false`. + * + * @returns - The current `Notation` instance (self). + * + * @throws {NotationError} - If strict notation is enabled, `overwrite` + * option is set to `false` and attempted to overwrite an existing value. + * + * @example + * const obj = { car: { brand: "Dodge", year: 1970 } }; + * Notation.create(obj) + * .set("car.brand", "Ford") + * .set("car.model", "Mustang") + * .set("car.year", 1965, false) + * .set("car.color", "red") + * .set("boat", "none"); + * console.log(obj); + * // { notebook: "Mac", car: { brand: "Ford", model: "Mustang", year: 1970, color: "red" }, boat: "none" }; + */ + set( + notation: string, + value: unknown, + mode: 'overwrite' | 'insert' | boolean = 'overwrite' + ): this { + if (!notation.trim()) { + throw new NotationError(ERR.NOTATION + `'${notation}'`); + } + + if (mode === true) mode = 'overwrite'; + let level = this._source; + + let currentIsLast: boolean; + let nCurrentNote: string | number; + let nNextNote: string | number | null; + let nextIsArrayNote: boolean; + let type: string; + const insertErrMsg = 'Cannot set value by inserting at index, on an object'; + + Notation.eachNote(notation, (levelNotation, note, index, list) => { + currentIsLast = index === list.length - 1; + nCurrentNote = nNextNote || utils.normalizeNote(note); + nNextNote = currentIsLast ? null : utils.normalizeNote(list[index + 1]); + + const parentNotation = Notation.parent(levelNotation); + // this is necessary when for example we set 'arr[1].x' of a sparse array + // [empty, empty, { x: 1 }]. it will complain that arr[1] is undefined and + // cannot set x. so we need to set the parent level to an empty object or + // array (depending on the current note). + if (!utils.isCollection(level) && parentNotation) { + level = (typeof nCurrentNote === 'number' ? [] : {}) as S; + this.set(parentNotation, level, 'overwrite'); + } + + type = utils.type(level); + if (type === 'array' && typeof nCurrentNote !== 'number') { + throw new NotationError( + `Cannot set string key '${note}' on array ${parentNotation || 'source'}` + ); + } + + // check if the property is at this level + if (utils.hasOwn(level, nCurrentNote, type)) { + // check if we're at the last level + if (currentIsLast) { + // if mode is "overwrite", assign the value. + if (mode === 'overwrite') { + level[nCurrentNote] = value; + } else if (mode === 'insert') { + if (type === 'array') { + level.splice(nCurrentNote, 0, value); + } else { + throw new NotationError(insertErrMsg); + } + } + // otherwise, will not overwrite + } else { + // if not last level; just re-reference the current level. + level = level[nCurrentNote]; + } + } else { + if (currentIsLast && type !== 'array' && mode === 'insert') { + throw new NotationError(insertErrMsg); + } + + // if next normalized note is a number, it indicates that the + // current note is actually an array. + nextIsArrayNote = typeof nNextNote === 'number'; + + // we don't have this property at this level so; if this is the + // last level, we set the value if not, we set an empty + // collection for the next level + level[nCurrentNote] = currentIsLast ? value : nextIsArrayNote ? [] : {}; + level = level[nCurrentNote]; + } + }); + + return this; + } + + /** + * Just like the `.set()` method but instead of a single notation string, + * an object of notations and values can be passed. Sets the value of each + * corresponding property at the given notation. If a property does not + * exist, it will be created and nested at the calculated level. If it + * exists; its value will be overwritten by default. + * + * @param notationsObject - The notations object to be processed. This can + * either be a regular object with non-dotted keys (which will be merged to + * the first/root level of the source object); or a flattened object with + * notated (dotted) keys. + * @param [overwrite=true] - Whether to overwrite a property if exists. If + * set to `false`, the value will be inserted if applicable. + * + * @returns - The current `Notation` instance (self). + * + * @example + * const obj = { car: { brand: "Dodge", year: 1970 } }; + * Notation.create(obj).merge({ + * "car.brand": "Ford", + * "car.model": "Mustang", + * "car.year": 1965, + * "car.color": "red", + * "boat": "none" + * }); + * console.log(obj); + * // { car: { brand: "Ford", model: "Mustang", year: 1970, color: "red" }, boat: "none" }; + */ + merge( + notationsObject: S extends UnknownObject ? Collection : unknown[], + overwrite: boolean = true + ): this { + const type = utils.type(notationsObject); + if (this._isArray && type !== 'array') { + throw new NotationError(ERR.NOTA_OBJ + 'Expected an array.'); + } + + if (!this._isArray && type !== 'object') { + throw new NotationError(ERR.NOTA_OBJ + 'Expected an object.'); + } + + let value; + utils.each(Object.keys(notationsObject), (notation) => { + value = notationsObject[notation]; + this.set(notation, value, overwrite); + }); + + return this; + } + + /** + * Removes the properties by the given list of notations from the source + * object, and returns the removed. Opposite of `merge()` method. + * + * @param notations - The notations array to be processed. + * + * @returns - An object with removed properties. + * + * @example + * const obj = { car: { brand: "Dodge", year: 1970 }, notebook: "Mac" }; + * const separated = Notation.create(obj).separate(["car.brand", "boat" ]); + * console.log(separated); + * // { notebook: "Mac", car: { brand: "Ford" } }; + * console.log(obj); + * // { car: { year: 1970 } }; + */ + separate(notations: string[]): UnknownObject { + // TODO: Should be S (Collection) + if (utils.type(notations) !== 'array') { + throw new NotationError(ERR.NOTA_OBJ + 'Expected an array.'); + } + + const o = new Notation({}); + utils.each(notations, (notation) => { + const result = this.inspectRemove(notation); + o.set(notation, result.value, 'overwrite'); + }); + + return o.value; + } + + /** + * Deep clones the source object while filtering its properties by the + * given glob notations. Includes all matched properties and removes + * the rest. + * + * The difference between regular notations and glob-notations is that; + * with the latter, you can use wildcard stars (*) and negate the notation + * by prepending a bang (!). A negated notation will be excluded. + * + * Order of the globs does not matter; they will be logically sorted. Loose + * globs will be processed first and verbose globs or normal notations will + * be processed last. e.g. `[ "car.model", "*", "!car.*" ]` will be + * normalized and sorted as `[ "*", "!car" ]`. + * + * Passing no parameters or passing a glob of `"!*"` or `["!*"]` will empty + * the source object. See `NotationGlob` class for more information. + * + * @param globList - Glob notation list to be processed. + * @param [options] - Filter options. + * + * @returns - The current `Notation` instance (self). To get the filtered + * value, call `.value` property on the instance. + * + * @example + * const car = { brand: "Ford", model: { name: "Mustang", year: 1970 } }; + * const n = Notation.create(car); + * + * console.log(n.filter([ "*", "!model.year" ]).value); // { brand: "Ford", model: { name: "Mustang" } } + * console.log(n.filter("model.name").value); // { model: { name: "Mustang" } } + * console.log(car); // { brand: "Ford", model: { name: "Mustang", year: 1970 } } + * console.log(n.filter().value); // {} // —» equivalent to n.filter("") or n.filter("!*") + */ + filter(globList: string | string[], options: INotationFilterOptions = {}): this { + const { re } = utils; + + // ensure array, normalize and sort the globs in logical order. this + // also concats the array first (to prevent mutating the original + // array). + const globs = NotationGlob.normalize(globList, Boolean(options?.restrictive)); + const len = globs.length; + const empty = (this._isArray ? [] : {}) as S; + + // if globs is "" or [""] or ["!*"] or ["![*]"] set source to empty and return. + if (len === 0 || (len === 1 && (!globs[0] || re.NEGATE_ALL.test(globs[0])))) { + this._source = empty; + return this; + } + + const cloned = utils.cloneDeep(this.value); + + const firstIsWildcard = re.WILDCARD.test(globs[0]); + // if globs only consist of "*" or "[*]"; set the "clone" as source and + // return. + if (len === 1 && firstIsWildcard) { + this._source = cloned; + return this; + } + + let filtered: Notation; + // if the first item of sorted globs is "*" or "[*]" we set the source + // to the (full) "copy" and remove the wildcard from globs (not to + // re-process). + if (firstIsWildcard) { + filtered = new Notation(cloned); + globs.shift(); + } else { + // otherwise we set an empty object or array source so that + // we can add notations/properties to it. + filtered = new Notation(empty); + } + // console.info('filtered:', filtered.value); + + // iterate through globs + utils.each(globs, (globNotation: string): void | false => { + const g = new NotationGlob(globNotation); + const { glob, absGlob, isNegated, notes } = g; + + let normalized: string; + let emptyValue: Collection | null = null; + let eType: string; + + // check whether the glob ends with `.*` or `[*]` then remove + // trailing glob note and decide for empty value (if negated). for + // non-negated, trailing wildcards are already removed by + // normalization. + if (absGlob.slice(-2) === '.*') { + normalized = absGlob.slice(0, -2); + /* istanbul ignore else -- non-negated trailing wildcards are normalized away before here. */ + if (isNegated) emptyValue = {}; + eType = 'object'; + } else if (absGlob.slice(-3) === '[*]') { + normalized = absGlob.slice(0, -3); + /* istanbul ignore else -- non-negated trailing wildcards are normalized away before here. */ + if (isNegated) emptyValue = []; + eType = 'array'; + } else { + normalized = absGlob; + } + + // we'll check glob vs value integrity if emptyValue is set; and throw if needed. + // eslint-disable-next-line max-len, @typescript-eslint/no-non-null-assertion + const errGlobIntegrity = `Integrity failed for glob '${glob}'. Cannot set empty ${eType!} for '${normalized}' which has a type of `; // ... + + // check if remaining normalized glob has no wildcard stars e.g. + // "a.b" or "!a.b.c" etc.. + if (re.WILDCARDS.test(normalized) === false) { + if (isNegated) { + // inspect and directly remove the notation if negated. + // we need the inspection for the detailed error below. + const insRemove = filtered.inspectRemove(normalized); + // console.log('insRemove', insRemove); + + // if original glob had `.*` at the end, it means remove + // contents (not itself). so we'll set an empty object. + // meaning `some.prop` (prop) is removed completely but + // `some.prop.*` (prop) results in `{}`. For array notation + // (`[*]`), we'll set an empty array. + if (emptyValue) { + // e.g. for glob `![0].x.*` we expect to set `[0].x = {}` + // but if `.x` is not an object (or array), we should fail. + const vType = insRemove.type; + const errMsg = errGlobIntegrity + `'${vType}'.`; + // in non-strict mode, only exceptions are `null` and + // `undefined`, for which we won't throw but we'll not + // set an empty obj/arr either. + + const isValSet = utils.isset(insRemove.value); + // on critical type mismatch we throw + // or if original value is undefined or null in strict mode we throw + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + if ((isValSet && vType !== eType!) || (!isValSet && this.options.strict)) { + throw new NotationError(errMsg); + } + // if parent is an array, we'll insert the value at + // index bec. we've removed the item and indexes are + // shifted. Otherwise, we'll simply overwrite the + // object property value. + const setMode = insRemove.parentIsArray ? 'insert' : 'overwrite'; + filtered.set(normalized, emptyValue, setMode); + } + } else { + // directly set the same notation from the original + const insGet = this.inspectGet(normalized); // Notation.create(original).inspectGet ... + if (insGet.has) { + filtered.set(normalized, insGet.value, 'overwrite'); + } + } + + // move to the next + return; + } + + // if glob has wildcard(s), we'll iterate through keys of the source + // object and see if (full) notation of each key matches the current + // glob. + + // important! we will iterate with eachRight to prevent shifted + // indexes when removing items from arrays. + const reverseIterateIfArray = true; + + _each( + this._source, + (originalNotation, _key, value): void | false => { + // console.info('» _each originalNotation:', originalNotation, '» key:', _key, '» value:', value); + const originalIsCovered = NotationGlob.create(normalized).covers(originalNotation); + // console.log('» normalized:', normalized, 'covers', originalNotation, '»', originalIsCovered); + if (!originalIsCovered) return; // break + + if (this.options.strict && emptyValue) { + // since original is covered and we have emptyValue set (due + // to trailing wildcard), here we'll check value vs glob + // integrity; (only if we're in strict mode). + + const vType = utils.type(value); + // types and number of levels are the same? + if ( + vType !== eType && + // we subtract 1 from number of levels bec. the last + // note is removed since we have emptyValue set. + Notation.split(originalNotation).length === notes.length - 1 + ) { + throw new NotationError(errGlobIntegrity + `'${vType}'.`); + } + } + + // iterating each note of original notation. i.e.: + // note1.note2.note3 is iterated from left to right, as: + // 'note1', 'note1.note2', 'note1.note2.note3' — in order. + Notation.eachNote(originalNotation, (levelNotation, _note, _index): void | false => { + if (g.test(levelNotation)) { + const levelLen = Notation.split(levelNotation).length; + if (isNegated && notes.length <= levelLen) { + // console.log(' » removing', levelNotation, 'of', originalNotation); + filtered.remove(levelNotation); + // we break and return early if removed bec. e.g. when + // 'note1.note2' (parent) of 'note1.note2.note3' is also removed, + // we no more have 'note3'. + return false; + } + } + if (levelNotation === originalNotation) { + // console.info('---> setting (c)', levelNotation, value, '\n filtered:', JSON.stringify(filtered.value)); + filtered.set(levelNotation, value, 'overwrite'); + } + }); + }, + reverseIterateIfArray + ); + }); + + // finally set the filtered source value of our instance and + // return. + this._source = filtered.value; + + return this; + } + + /** + * Removes the property from the source object, at the given notation. + * + * @param notation - The notation to be inspected. + * + * @returns - The current `Notation` instance (self). + * + * @throws {NotationError} - If `strict` option is enabled and notation + * does not exist. + * + * @example + * const obj = { notebook: "Mac", car: { model: "Mustang" } }; + * Notation.create(obj).remove("car.model"); + * console.log(obj); // { notebook: "Mac", car: { } } + */ + remove(notation: string): this { + const result = this.inspectRemove(notation); + + // if strict, check if implied index or prop exists + if (this.options.strict && !result.has) { + const msg = result.parentIsArray ? ERR.NO_INDEX : ERR.NO_PROP; + throw new NotationError(msg + `'${notation}'`); + } + + return this; + } + + /** + * Copies the notated property from the source collection and adds it to + * the destination — only if the source object actually has that property. + * This is different than a property with a value of `undefined`. + * + * @param destination - The destination object that the notated properties + * will be copied to. + * @param notation - The notation to get the corresponding property from + * the source object. + * @param [newNotation=null] - The notation to set the source property on + * the destination object. In other words, the copied property will be + * renamed to this value before set on the destination object. If not set, + * `notation` argument will be used. + * @param [overwrite=true] - Whether to overwrite the property on the + * destination object if it exists. + * + * @returns - The current `Notation` instance (self). + * + * @throws {NotationError} - If `destination` is not a valid collection. + * @throws {NotationError} - If `notation` or `newNotation` is invalid. + * + * @example + * const obj = { car: { brand: "Ford", model: "Mustang" } }; + * const models = { dodge: "Charger" }; + * Notation.create(obj).copyTo(models, "car.model", "ford"); + * console.log(models); + * // { dodge: "Charger", ford: "Mustang" } + * // source object (obj) is not modified + */ + copyTo( + destination: Collection, + notation: string, + newNotation?: string | null, + overwrite: boolean = true + ): this { + if (!utils.isCollection(destination)) { + throw new NotationError(ERR.DEST); + } + + const result = this.inspectGet(notation); + if (result.has) { + const newN = utils.getNewNotation(newNotation, notation); + Notation.create(destination).set(newN, result.value, overwrite); + } + + return this; + } + + /** + * Copies the notated property from the target collection and adds it to + * (own) source object — only if the target object actually has that + * property. This is different than a property with a value of `undefined`. + * + * @param target - The target collection that the notated properties will + * be copied from. + * @param notation - The notation to get the corresponding property from + * the target object. + * @param [newNotation=null] - The notation to set the copied property on + * our source collection. In other words, the copied property will be + * renamed to this value before set. If not set, `notation` argument will + * be used. + * @param [overwrite=true] - Whether to overwrite the property on our + * collection if it exists. + * + * @returns - The current `Notation` instance (self). + * + * @throws {NotationError} - If `target` is not a valid collection. + * @throws {NotationError} - If `notation` or `newNotation` is invalid. + * + * @example + * const obj = { car: { brand: "Ford", model: "Mustang" } }; + * const models = { dodge: "Charger" }; + * Notation.create(obj).copyFrom(models, "dodge", "car.model", true); + * console.log(obj); + * // { car: { brand: "Ford", model: "Charger" } } + * // models object is not modified + */ + copyFrom( + target: Collection, + notation: string, + newNotation?: string | null, + overwrite: boolean = true + ): this { + if (!utils.isCollection(target)) { + throw new NotationError(ERR.DEST); + } + + const result = Notation.create(target).inspectGet(notation); + if (result.has) { + const newN = utils.getNewNotation(newNotation, notation); + this.set(newN, result.value, overwrite); + } + + return this; + } + + /** + * Removes the notated property from the source (own) collection and adds + * it to the destination — only if the source collection actually has that + * property. This is different than a property with a value of `undefined`. + * + * @param destination - The destination collection that the notated + * properties will be moved to. + * @param notation - The notation to get the corresponding property from + * the source object. + * @param [newNotation=null] - The notation to set the source property on + * the destination object. In other words, the moved property will be + * renamed to this value before set on the destination object. If not set, + * `notation` argument will be used. + * @param [overwrite=true] - Whether to overwrite the property on the + * destination object if it exists. + * + * @returns - The current `Notation` instance (self). + * + * @throws {NotationError} - If `destination` is not a valid collection. + * @throws {NotationError} - If `notation` or `newNotation` is invalid. + * + * @example + * const obj = { car: { brand: "Ford", model: "Mustang" } }; + * const models = { dodge: "Charger" }; + * Notation.create(obj).moveTo(models, "car.model", "ford"); + * console.log(obj); + * // { car: { brand: "Ford" } } + * console.log(models); + * // { dodge: "Charger", ford: "Mustang" } + */ + moveTo( + destination: Collection, + notation: string, + newNotation?: string | null, + overwrite: boolean = true + ): this { + if (!utils.isCollection(destination)) { + throw new NotationError(ERR.DEST); + } + + const result = this.inspectRemove(notation); + if (result.has) { + const newN = utils.getNewNotation(newNotation, notation); + Notation.create(destination).set(newN, result.value, overwrite); + } + + return this; + } + + /** + * Removes the notated property from the target collection and adds it to + * (own) source collection — only if the target object actually has that + * property. This is different than a property with a value of `undefined`. + * + * @param target - The target collection that the notated properties will + * be moved from. + * @param notation - The notation to get the corresponding property from + * the target object. + * @param [newNotation=null] - The notation to set the target property on + * the source object. In other words, the moved property will be renamed to + * this value before set on the source object. If not set, `notation` + * argument will be used. + * @param [overwrite=true] - Whether to overwrite the property on the + * source object if it exists. + * + * @returns - The current `Notation` instance (self). + * + * @throws {NotationError} - If `target` is not a valid collection. + * @throws {NotationError} - If `notation` or `newNotation` is invalid. + * + * @example + * const obj = { car: { brand: "Ford", model: "Mustang" } }; + * const models = { dodge: "Charger" }; + * Notation.create(obj).moveFrom(models, "dodge", "car.model", true); + * console.log(obj); + * // { car: { brand: "Ford", model: "Charger" } } + * console.log(models); + * // {} + */ + moveFrom( + target: Collection, + notation: string, + newNotation?: string | null, + overwrite: boolean = true + ): this { + if (!utils.isCollection(target)) { + throw new NotationError(ERR.DEST); + } + + const result = Notation.create(target).inspectRemove(notation); + if (result.has) { + const newN = utils.getNewNotation(newNotation, notation); + this.set(newN, result.value, overwrite); + } + + return this; + } + + /** + * Renames the notated property of the source collection by the new + * notation. + * + * @param notation - The notation to get the corresponding property (value) + * from the source collection. + * @param newNotation - The new notation for the targeted property value. + * @param [overwrite=true] - Whether to overwrite the property at the new + * notation, if it exists. + * + * @returns - The current `Notation` instance (self). + * + * @throws {NotationError} - If `notation` or `newNotation` is invalid. + * + * @example + * const obj = { car: { brand: "Ford", model: "Mustang" } }; + * Notation.create(obj) + * .rename("car.brand", "carBrand") + * .rename("car.model", "carModel"); + * console.log(obj); + * // { carBrand: "Ford", carModel: "Mustang" } + */ + rename(notation: string, newNotation: string, overwrite: boolean = true): this { + return this.moveTo(this._source, notation, newNotation, overwrite); + } + + /** + * Extracts (copies) the property at the given notation to a new object by + * copying it from the source collection. This is equivalent to + * `.copyTo({}, notation, newNotation)`. + * + * @param notation - The notation to get the corresponding property (value) + * from the source object. + * @param [newNotation] - The new notation to be set on the new object for + * the targeted property value. If not set, `notation` argument will be + * used. + * + * @returns - A new object with the notated property. + * + * @throws {NotationError} - If `notation` or `newNotation` is invalid. + * + * @example + * const obj = { car: { brand: "Ford", model: "Mustang" } }; + * const extracted = Notation.create(obj).extract("car.brand", "carBrand"); + * console.log(extracted); // { carBrand: "Ford" } + * // obj is not modified + */ + extract(notation: string, newNotation?: string | null): UnknownObject { + const o = {}; + this.copyTo(o, notation, newNotation); + return o; + } + + /** + * Extrudes (moves) the property at the given notation to a new collection + * by moving it from the source collection. This is equivalent to + * `.moveTo({}, notation, newNotation)`. + * + * @param notation - The notation to get the corresponding property (value) + * from the source object. + * @param [newNotation] - The new notation to be set on the new object for + * the targeted property value. If not set, `notation` argument will be + * used. + * + * @returns - A new object with the notated property. + * + * @example + * const obj = { car: { brand: "Ford", model: "Mustang" } }; + * const extruded = Notation.create(obj).extrude("car.brand", "carBrand"); + * console.log(obj); + * // { car: { model: "Mustang" } } + * console.log(extruded); + * // { carBrand: "Ford" } + */ + extrude(notation: string, newNotation?: string | null): UnknownObject { + const o = {}; + this.moveTo(o, notation, newNotation); + return o; + } + + // -------------------------------- + // STATIC MEMBERS + // -------------------------------- + + /** + * Basically constructs a new `Notation` instance. + * + * @param [source={}] - The source collection to be notated. + * @param [options] - Notation options. + * + * @returns - The created `Notation` instance. + * + * @example + * const obj = { car: { brand: "Dodge", model: "Charger", year: 1970 } }; + * const notation = Notation.create(obj); // equivalent to new Notation(obj) + * notation.get('car.model') // » "Charger" + * notation.remove('car.model').set('car.color', 'red').value + * // » { car: { brand: "Dodge", year: 1970, color: "red" } } + */ + static create(source?: UnknownObject, options?: INotationOptions): Notation; + static create(source: unknown[], options?: INotationOptions): Notation; + static create(source?: Collection, options?: INotationOptions): Notation { + if (!source) { + return new Notation({}); + } + + return new Notation(source, options); + } + + /** + * Checks whether the given notation string is valid. Note that the star + * (`*`) (which is a valid character, even if irregular) is NOT treated as + * wildcard here. This checks for regular dot-notation, not a glob-notation. + * For glob notation validation, use `NotationGlob.isValid()` method. Same + * goes for the negation character/prefix (`!`). + * + * @param notation - The notation string to be checked. + * + * @example + * Notation.isValid('prop1.prop2.prop3'); // true + * Notation.isValid('x'); // true + * Notation.isValid('x.arr[0].y'); // true + * Notation.isValid('x["*"]'); // true + * Notation.isValid('x.*'); // false (this would be valid for Notation#filter() only or NotationGlob class) + * Notation.isValid('@1'); // false (should be "['@1']") + * Notation.isValid(null); // false + */ + static isValid(notation: string): boolean { + return typeof notation === 'string' && reVALIDATOR.test(notation); + } + + /** + * Splits the given notation string into its notes (levels). + * + * @param notation - Notation string to be splitted. + * @returns - A string array of notes (levels). + * + * @throws {NotationError} - If given notation is invalid. + */ + static split(notation: string): string[] { + if (!Notation.isValid(notation)) { + throw new NotationError(ERR.NOTATION + `'${notation}'`); + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return notation.match(reMATCHER)!; + } + + /** + * Joins the given notes into a notation string. + * + * @param notes - Notes (levels) to be joined. + * @returns - Joined notation string. + */ + static join(notes: string[]): string { + return utils.joinNotes(notes); + } + + /** + * Counts the number of notes/levels in the given notation. + * + * @param notation - The notation string to be processed. + * @returns - Number of notes. + * + * @throws {NotationError} - If given notation is invalid. + */ + static countNotes(notation: string): number { + return Notation.split(notation).length; + } + + /** + * Gets the first (root) note of the notation string. + * + * @param notation - The notation string to be processed. + * @returns - First note. + * + * @throws {NotationError} - If given notation is invalid. + * + * @example + * Notation.first('first.prop2.last'); // "first" + */ + static first(notation: string): string { + return Notation.split(notation)[0]; + } + + /** + * Gets the last note of the notation string. + * + * @param notation - The notation string to be processed. + * @returns - Last note. + * + * @throws {NotationError} - If given notation is invalid. + * + * @example + * Notation.last('first.prop2.last'); // "last" + */ + static last(notation: string): string { + const list = Notation.split(notation); + return list[list.length - 1]; + } + + /** + * Gets the parent notation (up to but excluding the last note) + * from the notation string. + * + * @param notation - The notation string to be processed. + * @returns - Parent note if any. Otherwise, `null`. + * + * @throws {NotationError} - If given notation is invalid. + * + * @example + * Notation.parent('first.prop2.last'); // "first.prop2" + * Notation.parent('single'); // null + */ + static parent(notation: string): string | null { + const last = Notation.last(notation); + return notation.slice(0, -last.length).replace(/\.$/, '') || null; + } + + /** + * Iterates through each note/level of the given notation string. + * + * @param notation - The notation string to be iterated through. + * @param callback - The callback function to be invoked on + * each iteration. To break out of the loop, return `false` from within the + * callback. + * Callback signature: `callback(levelNotation, note, index, list) { ... }` + * + * @throws {NotationError} - If given notation is invalid. + * + * @example + * const notation = 'first.prop2.last'; + * Notation.eachNote(notation, function (levelNotation, note, index, list) { + * console.log(index, note, levelNotation); + * }); + * // 0 "first" "first" + * // 1 "first.prop2" "prop2" + * // 2 "first.prop2.last" "last" + */ + static eachNote(notation: string, callback: NotationEachNoteCallback): void { + const notes = Notation.split(notation); + const levelNotes: string[] = []; + utils.each( + notes, + (note: string, index: number): void | false => { + levelNotes.push(note); + if (callback(Notation.join(levelNotes), note, index, notes) === false) return false; + }, + Notation + ); + } +} + +// -------------------------------- +// HELPERS +// -------------------------------- + +/** + * Deep iterates through each note (level) of each item in the given + * collection. + * @private + * + * @param collection - A data object or an array,he source. + * @param callback - A function to be executed on each iteration, + * with the following arguments: `(levelNotation, note, value, collection)` + * @param [reverseIfArray=false] - Set to `true` to iterate with + * `eachRight` to prevent shifted indexes when removing items from arrays. + * @param [byLevel=false] - Indicates whether to iterate notations by + * each level or by the end value. For example, if we have a collection of + * `{a: { b: true } }`, and `byLevel` is set; the callback will be invoked on + * the following notations: `a`, `a.b`. Otherwise, it will be invoked only on + * `a.b`. + * @param [parentNotation] - Storage for parent (previous) notation. + * @param [topSource] - Storage for initial/main collection. + */ +function _each( + collection: O | A[], + callback: NotationEachCallback, + reverseIfArray: boolean = false, + byLevel: boolean = false, + parentNotation: string | undefined | null = null, + topSource: O | A[] | undefined | null = null +): void { + const source = topSource || collection; + // if (!utils.isCollection(collection)) throw ... // no need + utils.eachItem( + collection, + (value: unknown, keyOrIndex: string | number): void | false => { + const note = typeof keyOrIndex === 'number' ? `[${keyOrIndex}]` : keyOrIndex; + const currentNotation = Notation.join([parentNotation as string, note]); + const isCollection = utils.isCollection(value); + // if it's not a collection we'll execute the callback. if it's a + // collection but byLevel is set, we'll also execute the callback. + if (!isCollection || byLevel) { + if (callback(currentNotation, note, value, source) === false) return false; + } + // deep iterating if collection + if (isCollection) + _each(value as O | A[], callback, reverseIfArray, byLevel, currentNotation, source); + }, + null, + reverseIfArray + ); +} diff --git a/src/core/NotationError.ts b/src/core/NotationError.ts new file mode 100644 index 0000000..8bbf555 --- /dev/null +++ b/src/core/NotationError.ts @@ -0,0 +1,47 @@ +/* eslint no-prototype-builtins:0 */ + +const setProto = Object.setPrototypeOf; + +/** + * Error class specific to `Notation`. + */ +export class NotationError extends Error { + /** + * Initializes a new `NotationError` instance. + * @param message - The error message. + */ + constructor(message: string = '') { + super(message); + setProto(this, NotationError.prototype); + + Object.defineProperty(this, 'name', { + enumerable: false, + writable: false, + value: 'NotationError' + }); + + Object.defineProperty(this, 'message', { + enumerable: false, + writable: true, + value: message + }); + + // Stryker disable all: stack-trace wiring is environment-defensive with no + // behavioral effect (a stack exists either way); the non-V8 branch is + // unreachable in supported runtimes. + /* istanbul ignore else -- @preserve: captureStackTrace always exists in V8/Node */ + if (Object.hasOwn(Error, 'captureStackTrace')) { + // V8 + Error.captureStackTrace(this, NotationError); + } else { + /* istanbul ignore start */ + Object.defineProperty(this, 'stack', { + enumerable: false, + writable: false, + value: new Error(message).stack + }); + } + /* istanbul ignore stop */ + // Stryker restore all + } +} diff --git a/src/core/NotationGlob.ts b/src/core/NotationGlob.ts new file mode 100644 index 0000000..90be9e1 --- /dev/null +++ b/src/core/NotationGlob.ts @@ -0,0 +1,1123 @@ +/* eslint-disable max-lines */ + +import type { UnknownObject } from '../types.js'; +import { utils } from '../utils.js'; +import type { INotationGlobInspection } from './INotationGlobInspection.js'; +import { Notation } from './Notation.js'; +import { NotationError } from './NotationError.js'; + +// http://www.linfo.org/wildcard.html +// http://en.wikipedia.org/wiki/Glob_%28programming%29 +// http://en.wikipedia.org/wiki/Wildcard_character#Computing + +// created test @ https://regex101.com/r/U08luj/2 +const reMATCHER = /(\[(\d+|\*|".*"|'.*')\]|[a-z$_][a-z$_\d]*|\*)/gi; // ! negation should be removed first +const sreJsVAR = '[a-z$_][a-z$_\\d]*'; +const reJsVAR = /[a-z$_][a-z$_\d]*/i; +// array index or wildcard, or object bracket notation +const sreBRACKET = '\\[(\\d+|\\*|".*"|\'.*\')\\]'; +const reBRACKET = /\[(\d+|\*|".*"|'.*')\]/; +// created test @ https://regex101.com/r/mC8unE/3 +// /^!?(\*|[a-z$_][a-z$_\d]*|\[(\d+|".*"|'.*'|`.*`|\*)\])(\[(\d+|".*"|'.*'|`.*`|\*)\]|\.[a-z$_][a-z$_\d]*|\.\*)*$/i +const reVALIDATOR = new RegExp( + '^' + + '!?(' + // optional negation, only in the front + '\\*' + // wildcard star + '|' + // OR + sreJsVAR + // JS variable syntax + '|' + // OR + sreBRACKET + // array index or wildcard, or object bracket notation + ')' + // exactly once + '(' + + sreBRACKET + // followed by same + '|' + // OR + '\\.' + + sreJsVAR + // dot, then JS variable syntax + '|' + // OR + '\\.\\*' + // dot, then wildcard star + ')*' + // (both) may repeat any number of times + '$', + 'i' +); + +const { re } = utils; +const ERR_INVALID = 'Invalid glob notation: '; + +/** + * `NotationGlob` is a utility for validating, comparing and sorting + * dot-notation globs. + * + * You can use {@link http://www.linfo.org/wildcard.html|wildcard} stars `*` + * and negate the notation by prepending a bang `!`. A star will include all + * the properties at that level and a negated notation will be excluded. + * + * @example + * // for the following object; + * { name: 'John', billing: { account: { id: 1, active: true } } }; + * + * 'billing.account.*' // represents value `{ id: 1, active: true }` + * 'billing.account.id' // represents value `1` + * '!billing.account.*' // represents value `{ name: 'John' }` + * 'name' // represents `'John'` + * '*' // represents the whole object + * + * @example + * const glob = new NotationGlob('billing.account.*'); + * glob.test('billing.account.id'); // true + */ +export class NotationGlob { + private _: { + glob: string; + absGlob: string; + isNegated: boolean; + regexp?: RegExp; + notes: string[]; + parent?: string | null; + }; + + /** + * Constructs a `NotationGlob` object with the given glob string. + * @param glob - Notation string with globs. + * + * @throws {NotationError} - If given notation glob is invalid. + */ + constructor(glob: string) { + const ins = NotationGlob._inspect(glob); + const notes = NotationGlob.split(ins.absGlob); + this._ = { + ...ins, + notes, + // below props will be set at first getter call + parent: undefined, // don't set to null + regexp: undefined + }; + } + + // -------------------------------- + // INSTANCE PROPERTIES + // -------------------------------- + + /** + * Gets the normalized glob notation string. + */ + get glob(): string { + return this._.glob; + } + + /** + * Gets the absolute glob notation without the negation prefix `!` and + * redundant trailing wildcards. + */ + get absGlob(): string { + return this._.absGlob; + } + + /** + * Specifies whether this glob is negated with a `!` prefix. + */ + get isNegated(): boolean { + return this._.isNegated; + } + + /** + * Represents this glob in regular expressions. + * Note that the negation prefix (`!`) is ignored, if any. + */ + get regexp(): RegExp { + // setting on first call instead of in constructor, for performance + // optimization. + this._.regexp = this._.regexp || NotationGlob.toRegExp(this.absGlob); + return this._.regexp; + } + + /** + * List of notes (levels) of this glob notation. Note that trailing, + * redundant wildcards are removed from the original glob notation. + */ + get notes(): string[] { + return this._.notes; + } + + /** + * Gets the first note of this glob notation. + */ + get first(): string { + return this.notes[0]; + } + + /** + * Gets the last note of this glob notation. + */ + get last(): string { + return this.notes[this.notes.length - 1]; + } + + /** + * Gets the parent notation (up to but excluding the last note) from the + * glob notation string. Note that initially, trailing/redundant wildcards + * are removed. + * + * @example + * const glob = NotationGlob.create; + * glob('first.second.*').parent; // "first.second" + * glob('*.x.*').parent; // "*" ("*.x.*" normalizes to "*.x") + * glob('*').parent; // null (no parent) + */ + get parent(): string | null { + // setting on first call instead of in constructor, for performance + // optimization. + if (this._.parent === undefined) { + this._.parent = + this.notes.length > 1 ? this.absGlob.slice(0, -this.last.length).replace(/\.$/, '') : null; + } + + return this._.parent; + } + + // -------------------------------- + // INSTANCE METHODS + // -------------------------------- + + /** + * Checks whether the given notation value matches the source notation + * glob. + * + * @param {String} notation - The notation string to be tested. Cannot have + * any globs. + * + * @throws {NotationError} - If given `notation` is not valid or contains + * any globs. + * + * @example + * const glob = new NotationGlob('!prop.*.name'); + * glob.test("prop.account.name"); // true + */ + test(notation: string): boolean { + if (!Notation.isValid(notation)) { + throw new NotationError(`Invalid notation: '${notation}'`); + } + // return this.regexp.test(notation); + return NotationGlob._covers(this, notation); + } + + /** + * Specifies whether this glob notation can represent (or cover) the given + * glob notation. Note that negation prefix is ignored, if any. + * + * @param glob - Glob notation string, glob notes array or a `NotationGlob` + * instance. + * + * @example + * const glob = NotationGlob.create; + * glob('*.y').covers('x.y') // true + * glob('x[*].y').covers('x[*]') // false + */ + covers(glob: string | string[] | NotationGlob): boolean { + // join into a glob string if given is an array of notes + const glb = Array.isArray(glob) ? NotationGlob.join(glob) : glob; + return NotationGlob._covers(this, glb); + } + + /** + * Gets the intersection of this and the given glob notations. When + * restrictive, if any one of them is negated, the outcome is negated. + * Otherwise, only if both of them are negated, the outcome is negated. + * + * @param glob - Second glob to be used. + * @param [restrictive=false] - Whether the intersection should be negated + * when one of the globs is negated. + * @returns - Intersection notation if any; otherwise `null`. + * + * @example + * const glob = NotationGlob.create; + * glob('x.*').intersect('!*.y') // 'x.y' + * glob('x.*').intersect('!*.y', true) // '!x.y' + */ + intersect(glob: string, restrictive: boolean = false): string | null { + return NotationGlob._intersect(this.glob, glob, restrictive); + } + + // -------------------------------- + // STATIC MEMBERS + // -------------------------------- + + /** + * Basically constructs a new `NotationGlob` instance with the given glob + * string. + * + * @param glob - The source notation glob. + * + * @example + * const glob = NotationGlob.create(strGlob); + * // equivalent to: + * const glob = new NotationGlob(strGlob); + */ + static create(glob: string): NotationGlob { + return new NotationGlob(glob); + } + + // Created test at: https://regex101.com/r/tJ7yI9/4 + /** + * Validates the given notation glob. + * + * @param glob - Notation glob to be validated. + */ + static isValid(glob: string): boolean { + return typeof glob === 'string' && reVALIDATOR.test(glob); + } + + /** + * Validates the given glob note. + * + * @param note - Note to be validated. + */ + static isValidNote(note: string): boolean { + return typeof note === 'string' && (note === '*' || reJsVAR.test(note) || reBRACKET.test(note)); + } + + /** + * Specifies whether the given glob notation includes any valid wildcards + * (`*`) or negation bang prefix (`!`). + * + * @param glob - Glob notation to be checked. + */ + static hasMagic(glob: string): boolean { + return NotationGlob.isValid(glob) && (re.WILDCARDS.test(glob) || glob[0] === '!'); + } + + /** + * Gets a regular expressions instance from the given glob notation. + * Note that the bang `!` prefix will be ignored if the given glob is negated. + * + * @param glob - Glob notation to be converted. + * + * @returns - A `RegExp` instance from the glob. + * + * @throws {NotationError} - If given notation glob is invalid. + */ + static toRegExp(glob: string): RegExp { + if (!NotationGlob.isValid(glob)) { + throw new NotationError(`${ERR_INVALID} '${glob}'`); + } + + let g = glob.indexOf('!') === 0 ? glob.slice(1) : glob; + g = utils + .pregQuote(g) + // `[*]` always represents array index e.g. `[1]`. so we'd replace + // `\[\*\]` with `\[\d+\]` but we should also watch for quotes: e.g. + // `["x[*]y"]` + .replace(/\\\[\\\*\\\](?=(?:[^"]|"[^"]*")*$)(?=(?:[^']|'[^']*')*$)/g, '\\[\\d+\\]') + // `*` within quotes (e.g. ['*']) is non-wildcard, just a regular star char. + // `*` outside of quotes is always JS variable syntax e.g. `prop.*` + .replace(/\\\*(?=(?:[^"]|"[^"]*")*$)(?=(?:[^']|'[^']*')*$)/g, '[a-z$_][a-z$_\\d]*') + .replace(/\\\?/g, '.'); + + return new RegExp('^' + g + '(?:[\\[\\.].+|$)', 'i'); + // it should either end ($) or continue with a dot or bracket. So for + // example, `company.*` will produce `/^company\.[a-z$_][a-z$_\\d]*(?:[\\[|\\.].+|$)/` + // which will match both `company.name` and `company.address.street` but + // will not match `some.company.name`. Also `!password` will not match + // `!password_reset`. + } + + /** + * Splits the given glob notation string into its notes (levels). Note that + * this will exclude the `!` negation prefix, if it exists. + * + * @param glob - Glob notation string to be splitted. + * @param [normalize=false] - Whether to remove trailing, redundant + * wildcards. + * + * @returns - A string array of glob notes (levels). + * + * @throws {NotationError} - If given glob notation is invalid. + * + * @example + * NotationGlob.split('*.list[2].prop') // ['*', 'list', '[2]', 'prop'] + * // you can get the same result from the .notes property of a NotationGlob instance. + */ + static split(glob: string, normalize: boolean = false): string[] { + if (!NotationGlob.isValid(glob)) { + throw new NotationError(`${ERR_INVALID} '${glob}'`); + } + const neg = glob[0] === '!'; + // trailing wildcards are redundant only when not negated + const g = !neg && normalize ? utils.removeTrailingWildcards(glob) : glob; + /* istanbul ignore next -- `|| []` is defensive: `glob` already passed isValid(). */ + return g.replace(/^!/, '').match(reMATCHER) || []; + } + + /** + * Joins the given notes into a glob notation. + * + * No negation is allowed in `notes` array. + * + * @param notes - Array of notes. + * @param [normalize=false] - Whether to remove trailing wildcards. + * + * @returns - A string of glob notation. + * + * @throws {NotationError} - If given notes are invalid. + */ + static join(notes: string[], normalize: boolean = false): string { + let glob = ''; + notes.forEach((note: string, index: number) => { + if (note === '*' || reJsVAR.test(note)) { + glob += index > 0 ? `.${note}` : note; + } else if (reBRACKET.test(note)) { + glob += note; + } else { + throw new NotationError(`Invalid note: '${glob}'`); + } + }); + + // trailing wildcards are redundant only when not negated. Since we + // don't allow negation in notes array here, it's user's responsibility + // to `normalize` or not. + return normalize ? utils.removeTrailingWildcards(glob) : glob; + } + + /** + * Compares two given notation globs and returns an integer value as a + * result. This is generally used to sort glob arrays. Loose globs (with + * stars especially closer to beginning of the glob string) and globs + * representing the parent/root of the compared property glob come first. + * Verbose/detailed/exact globs come last. (`* < *.abc < abc`). + * + * For instance; `store.address` comes before `store.address.street`. So + * this works both for `*, store.address.street, !store.address` and `*, + * store.address, !store.address.street`. For cases such as `prop.id` vs + * `!prop.id` which represent the same property; the negated glob comes + * last. + * + * @param globA - First notation glob to be compared. + * @param globB - Second notation glob to be compared. + * + * @returns - Returns `-1` if `globA` comes first, `1` if `globB` comes + * first and `0` if equivalent priority. + * + * @throws {NotationError} - If either `globA` or `globB` is invalid glob + * notation. + * + * @example + * const { compare } = NotationGlob; + * compare('*', 'info.user') // -1 + * compare('*', '[*]') // 0 + * compare('info.*.name', 'info.user') // 1 + */ + static compare(globA: string, globB: string): number { + // trivial case, both are exactly the same! + // or both are wildcard e.g. `*` or `[*]` + if (globA === globB || (re.WILDCARD.test(globA) && re.WILDCARD.test(globB))) return 0; + + const a = new NotationGlob(globA); + const b = new NotationGlob(globB); + + // Check depth (number of levels) + if (a.notes.length === b.notes.length) { + // check and compare if these are globs that represent items in the + // "same" array. if not, this will return 0. + const aIdxCompare = NotationGlob._compareArrayItemGlobs(a, b); + // we'll only continue comparing if 0 is returned + if (aIdxCompare !== 0) return aIdxCompare; + + // count wildcards + const wildCountA = (a.absGlob.match(re.WILDCARDS) || []).length; + const wildCountB = (b.absGlob.match(re.WILDCARDS) || []).length; + if (wildCountA === wildCountB) { + // check for negation + if (!a.isNegated && b.isNegated) return -1; + if (a.isNegated && !b.isNegated) return 1; + // both are negated or neither are, return alphabetical + return a.absGlob < b.absGlob ? -1 : a.absGlob > b.absGlob ? 1 : 0; + } + return wildCountA > wildCountB ? -1 : 1; + } + + return a.notes.length < b.notes.length ? -1 : 1; + } + + /** + * Sorts the notation globs in the given array by their priorities. Loose + * globs (with stars especially closer to beginning of the glob string); + * globs representing the parent/root of the compared property glob come + * first. Verbose/detailed/exact globs come last. (`* < *.y < x.y`). + * + * For instance; `store.address` comes before `store.address.street`. For + * cases such as `prop.id` vs `!prop.id` which represent the same property; + * the negated glob wins (comes last). + * + * @param globList - The notation globs array to be sorted. The passed + * array reference is modified. + * @returns - Logically sorted globs array. + * + * @example + * NotationGlob.sort(['!prop.*.name', 'prop.*', 'prop.id']) // ['prop.*', 'prop.id', '!prop.*.name']; + */ + static sort(globList: string[]): string[] { + return globList.sort(NotationGlob.compare); + } + + /** + * Normalizes the given notation globs array by removing duplicate or + * redundant items, eliminating extra verbosity (also with intersection + * globs) and returns a priority-sorted globs array. + * + *
    + *
  • If any exact duplicates found, all except first is removed. + *
    example: `['car', 'dog', 'car']` normalizes to `['car', 'dog']`.
  • + *
  • If both normal and negated versions of a glob are found, negated wins. + *
    example: `['*', 'id', '!id']` normalizes to `['*', '!id']`.
  • + *
  • If a glob is covered by another, it's removed. + *
    example: `['car.*', 'car.model']` normalizes to `['car']`.
  • + *
  • If a negated glob is covered by another glob, it's kept. + *
    example: `['*', 'car', '!car.model']` normalizes as is.
  • + *
  • If a negated glob is not covered by another or it does not cover any other; + * then we check for for intersection glob. If found, adds them to list; + * removes the original negated. + *
    example: `['car.*', '!*.model']` normalizes as to `['car', '!car.model']`.
  • + *
  • In restrictive mode; if a glob is covered by another negated glob, it's removed. + * Otherwise, it's kept. + *
    example: `['*', '!car.*', 'car.model']` normalizes to `['*', '!car']` if restrictive.
  • + *
+ * + * @param globList - Notation globs array to be normalized. + * @param [restrictive=false] - Whether negated items strictly remove every + * match. Note that, regardless of this option, if any item has an exact + * negated version; non-negated is always removed. + * + * @throws {NotationError} - If any item in globs list is invalid. + * + * @example + * const { normalize } = NotationGlob; + * normalize(['*', '!id', 'name', '!car.model', 'car.*', 'id', 'name']); // ['*', '!id', '!car.model'] + * normalize(['!*.id', 'user.*', 'company']); // ['company', 'user', '!company.id', '!user.id'] + * normalize(['*', 'car.model', '!car.*']); // ["*", "!car.*", "car.model"] + * // restrictive normalize: + * normalize(['*', 'car.model', '!car.*'], true); // ["*", "!car.*"] + */ + static normalize(globList: string | string[], restrictive: boolean = false): string[] { + const { _inspect, _covers, _intersect } = NotationGlob; + + const original = utils.ensureArray(globList); + if (original.length === 0) return []; + + const list = [...original] // prevent mutation + // move negated globs to top so that we inspect non-negated globs + // against others first. when complete, we'll sort with our + // .compare() function. + .sort(restrictive ? _negFirstSort : _negLastSort) + // turning string array into inspect-obj array, so that we'll not + // run _inspect multiple times in the inner loop. this also + // pre-validates each glob. + .map(_inspect); + + // early return if we have a single item + if (list.length === 1) { + const g = list[0]; + // single negated item is redundant + if (g.isNegated) return []; + // return normalized + return [g.glob]; + } + + // flag to return an empty array (in restrictive mode), if true. + let negateAll = false; + + // we'll push keepers in this array + let normalized: string[] = []; + // we'll need to remember excluded globs, so that we can move to next + // item early. + const ignored = {}; + + // storage to keep intersections. + // using an object to prevent duplicates. + const intersections = {}; + + const checkAddIntersection = (gA: string, gB: string): void => { + const inter = _intersect(gA, gB, restrictive); + if (!inter) return; + + // if the intersection result has an inverted version in the + // original list, don't add this. + if (!restrictive && original.indexOf(_invert(inter)) >= 0) return; + intersections[inter] = inter; + }; + + // iterate each glob by comparing it to remaining globs. + utils.eachRight(list, (a, indexA): void | false => { + // if `strict` is enabled, return empty if a negate-all is found + // (which itself is also redundant if single): '!*' or '![*]' + if (re.NEGATE_ALL.test(a.glob)) { + negateAll = true; + if (restrictive) return false; + } + + // flags + let duplicate = false; + let hasExactNeg = false; + // flags for negated + let negCoversPos = false; + let negCoveredByPos = false; + let negCoveredByNeg = false; + // flags for non-negated (positive) + let posCoversPos = false; + let posCoveredByNeg = false; + let posCoveredByPos = false; + + utils.eachRight(list, (b, indexB): void | false => { + // console.info('---> ', a.glob, 'vs', b.glob); + // don't inspect glob with itself + if (indexA === indexB) return; // move to next + // console.log(indexA, a.glob, 'vs', b.glob); + + // e.g. ['x.y.z', '[1].x', 'c'] » impossible! the tested source cannot + // be both an array and an object. + if (a.isArrayGlob !== b.isArrayGlob) { + // eslint-disable-next-line max-len + throw new NotationError( + `Integrity failed. Cannot have both object and array notations for root level: ${JSON.stringify(original)}` + ); + } + + // remove if duplicate + if (a.glob === b.glob) { + list.splice(indexA, 1); + duplicate = true; + return false; // break out + } + + // remove if positive has an exact negated (negated wins when + // normalized) e.g. ['*', 'a', '!a'] => ['*', '!a'] + if (!a.isNegated && _isReverseOf(a, b)) { + // list.splice(indexA, 1); + ignored[a.glob] = true; + hasExactNeg = true; + return false; // break out + } + + // if already excluded b, go on to next + if (ignored[b.glob]) return; // next + + const coversB = _covers(a, b); + const coveredByB = coversB ? false : _covers(b, a); + if (a.isNegated) { + if (b.isNegated) { + // if negated (a) covered by any other negated (b); remove (a)! + if (coveredByB) { + negCoveredByNeg = true; + // list.splice(indexA, 1); + ignored[a.glob] = true; + return false; // break out + } + } else { + /* istanbul ignore next */ + if (coversB) negCoversPos = true; + if (coveredByB) negCoveredByPos = true; + // try intersection if none covers the other and only + // one of them is negated. + if (!coversB && !coveredByB) { + checkAddIntersection(a.glob, b.glob); + } + } + } else { + if (b.isNegated) { + // if positive (a) covered by any negated (b); remove (a)! + if (coveredByB) { + posCoveredByNeg = true; + if (restrictive) { + // list.splice(indexA, 1); + ignored[a.glob] = true; + return false; // break out + } + return; // next + } + // try intersection if none covers the other and only + // one of them is negated. + if (!coversB && !coveredByB) { + checkAddIntersection(a.glob, b.glob); + } + } else { + if (coversB) posCoversPos = coversB; + // if positive (a) covered by any other positive (b); remove (a)! + if (coveredByB) { + posCoveredByPos = true; + if (restrictive) { + // list.splice(indexA, 1); + return false; // break out + } + } + } + } + }); + + // const keepNeg = (negCoversPos || negCoveredByPos) && !negCoveredByNeg; + const keepNeg = restrictive + ? (negCoversPos || negCoveredByPos) && negCoveredByNeg === false + : negCoveredByPos && negCoveredByNeg === false; + const keepPos = restrictive + ? (posCoversPos || posCoveredByPos === false) && posCoveredByNeg === false + : posCoveredByNeg || posCoveredByPos === false; + const keep = + duplicate === false && hasExactNeg === false && (a.isNegated ? keepNeg : keepPos); + + if (keep) { + normalized.push(a.glob); + } else { + // this is excluded from final (normalized) list, so mark as + // ignored (don't remove from "list" for now) + ignored[a.glob] = true; + } + }); + + if (restrictive && negateAll) return []; + + const interKeys = Object.keys(intersections); + if (interKeys.length > 0) { + // merge normalized list with intersections if any + normalized = [...normalized, ...interKeys]; + // we have new (intersection) items, so re-normalize + return NotationGlob.normalize(normalized, restrictive); + } + + return NotationGlob.sort(normalized); + } + + /** + * Gets the union from the given couple of glob arrays and returns a new + * array of globs. + *
    + *
  • If the exact same element is found in both + * arrays, one of them is removed to prevent duplicates. + *
    example: `['!id', 'name'] ∪ ['!id']` unites to `['!id', 'name']`
  • + *
  • If any non-negated item is covered by a glob in the same + * or other array, the redundant item is removed. + *
    example: `['*', 'name'] ∪ ['email']` unites to `['*']`
  • + *
  • If one of the arrays contains a negated equivalent of an + * item in the other array, the negated item is removed. + *
    example: `['!id'] ∪ ['id']` unites to `['id']`
  • + *
  • If any item covers/matches a negated item in the other array, + * the negated item is removed. + *
    example #1: `['!user.id'] ∪ ['user.*']` unites to `['user']` + *
    example #2: `['*'] ∪ ['!password']` unites to `['*']` + *
  • + *
+ * + * @param globsA - First array of glob strings. + * @param globsB - Second array of glob strings. + * @param [restrictive=false] - Whether negated items in each of the lists, + * strictly remove every match in themselves (not the cross list). This + * option is used when pre-normalizing each glob list and normalizing the + * final union list. + * + * @example + * const a = ['user.*', '!user.email', 'car.model', '!*.id']; + * const b = ['!*.date', 'user.email', 'car', '*.age']; + * const { union } = NotationGlob; + * union(a, b) // ['car', 'user', '*.age', '!car.date', '!user.id'] + */ + static union(globsA: string[], globsB: string[], restrictive: boolean = false): string[] { + const { normalize, _compareUnion } = NotationGlob; + + const listA = normalize(globsA, restrictive); + const listB = normalize(globsB, restrictive); + + if (listA.length === 0) return listB; + if (listB.length === 0) return listA; + + // TODO: below should be optimized + let union = _compareUnion(listA, listB, restrictive); + union = _compareUnion(listB, listA, restrictive, union); + return normalize(union, restrictive); + } + + /** + * Undocumented. See `.union()` + * @private + * + * @param globsListA - + * @param globsListB - + * @param restrictive - + * @param union - + */ + private static _compareUnion( + globsListA: string[], + globsListB: string[], + restrictive: boolean, + union: string[] = [] + ): string[] { + const { _covers } = NotationGlob; + + const { _inspect, _intersect } = NotationGlob; + + utils.eachRight(globsListA, (globA) => { + if (union.indexOf(globA) >= 0) return; // next + + const a = _inspect(globA); + + // if wildcard only, add... + if (re.WILDCARD.test(a.absGlob)) { + union.push(a.glob); // push normalized glob + return; // next + } + + let notCovered = false; + let hasExact = false; + let negCoversNeg = false; + let posCoversNeg = false; + let posCoversPos = false; + let negCoversPos = false; + + const intersections: string[] = []; + + utils.eachRight(globsListB, (globB) => { + // keep if has exact in the other + if (globA === globB) hasExact = true; + + const b = _inspect(globB); + + // keep negated if: + // 1) any negated covers it + // 2) no positive covers it + // keep positive if: + // 1) no positive covers it OR any negated covers it + + notCovered = !_covers(b, a); + if (notCovered) { + if (a.isNegated && b.isNegated) { + const inter = _intersect(a.glob, b.glob, restrictive); + if (inter && union.indexOf(inter) === -1) intersections.push(inter); + } + return; // next + } + + if (a.isNegated) { + if (b.isNegated) { + negCoversNeg = !hasExact; + } else { + posCoversNeg = true; // set flag + } + } else { + if (!b.isNegated) { + posCoversPos = !hasExact; + } else { + negCoversPos = true; // set flag + } + } + }); + + const keep = a.isNegated ? !posCoversNeg || negCoversNeg : !posCoversPos || negCoversPos; + + if (hasExact || keep || (notCovered && !a.isNegated)) { + union.push(a.glob); // push normalized glob + return; + } + + if (a.isNegated && posCoversNeg && !negCoversNeg && intersections.length > 0) { + union = [...union, ...intersections]; + } + }); + + return union; + } + + /** + * Used by static `_covers()` + * @private + */ + private static _coversNote(a: string, b: string): boolean { + if (!a || !b) return false; // glob e.g.: [2] does not cover [2][1] + const bIsArr = re.ARRAY_GLOB_NOTE.test(b); + // obj-wildcard a will cover b if not array + if (a === '*') return !bIsArr; + // arr-wildcard a will cover b if array + if (a === '[*]') return bIsArr; + // seems, a is not wildcard so, + // if b is wildcard (obj or arr) won't be covered + if (re.WILDCARD.test(b)) return false; + // normalize both and check for equality + // e.g. x.y and x['y'] are the same + return utils.normalizeNote(a) === utils.normalizeNote(b); + } + + /** + * Used by static `_covers()` + * @private + */ + private static _matchesNote(a: string, b: string): boolean { + if (!a || !b) return true; // glob e.g.: [2][1] matches [2] and vice-versa. + return NotationGlob._coversNote(a, b) || NotationGlob._coversNote(b, a); + } + + /** + * Specifies whether first glob notation can represent (or cover) the + * second. + * @private + * + * @param globA - Source glob notation string or inspection result object + * or `NotationGlob` instance. + * @param globB - Glob notation string or inspection result object or + * `NotationGlob` instance. + * @param [match=false] Check whether notes match instead of `globA` + * covers `globB`. + * + * @example + * const { covers } = NotationGlob; + * covers('*.y', 'x.y') // true + * covers('x.y', '*.y') // false + * covers('x.y', '*.y', true) // true + * covers('x[*].y', 'x[*]') // false + */ + private static _covers( + globA: string | INotationGlobInspection | NotationGlob, + globB: string | INotationGlobInspection | NotationGlob, + match: boolean = false + ): boolean { + const a: NotationGlob = + typeof globA === 'string' + ? new NotationGlob(globA) + : !Array.isArray((globA as UnknownObject).notes) + ? new NotationGlob(globA.glob) + : (globA as NotationGlob); + + const b: NotationGlob = + typeof globB === 'string' + ? new NotationGlob(globB) + : !Array.isArray((globB as UnknownObject).notes) + ? new NotationGlob(globB.glob) + : (globB as NotationGlob); + + /* istanbul ignore next -- `a`/`b` are normalized to NotationGlob above, so `.notes` is always set. */ + const notesA = a.notes || NotationGlob.split(a.absGlob); + /* istanbul ignore next -- see above. */ + const notesB = b.notes || NotationGlob.split(b.absGlob); + + if (!match) { + // !x.*.* does not cover !x.* or x.* bec. !x.*.* ≠ x.* ≠ x + // x.*.* covers x.* bec. x.*.* = x.* = x + if (a.isNegated && notesA.length > notesB.length) return false; + } + + let covers = true; + const fn = match ? NotationGlob._matchesNote : NotationGlob._coversNote; + for (let i = 0; i < notesA.length; i++) { + if (!fn(notesA[i], notesB[i])) { + covers = false; + break; + } + } + return covers; + } + + /** + * Gets the intersection notation of two glob notations. When restrictive, + * if any one of them is negated, the outcome is negated. Otherwise, only + * if both of them are negated, the outcome is negated. + * @private + * + * @param globA - First glob to be used. + * @param globB - Second glob to be used. + * @param [restrictive=false] - Whether the intersection should be negated + * when one of the globs is negated. + * + * @returns - Intersection notation if any; otherwise `null`. + * + * @example + * _intersect('!*.y', 'x.*', false) // 'x.y' + * _intersect('!*.y', 'x.*', true) // '!x.y' + */ + private static _intersect( + globA: string, + globB: string, + restrictive: boolean = false + ): string | null { + // const bang = restrictive + // ? (globA[0] === '!' || globB[0] === '!' ? '!' : '') + // : (globA[0] === '!' && globB[0] === '!' ? '!' : ''); + + const notesA = NotationGlob.split(globA, true); + const notesB = NotationGlob.split(globB, true); + + let bang; + if (restrictive) { + bang = globA[0] === '!' || globB[0] === '!' ? '!' : ''; + } else { + if (globA[0] === '!' && globB[0] === '!') { + bang = '!'; + } else { + bang = + (notesA.length > notesB.length && globA[0] === '!') || + (notesB.length > notesA.length && globB[0] === '!') + ? '!' + : ''; + } + } + + const len = Math.max(notesA.length, notesB.length); + let notesI: string[] = []; + let a: string; + let b: string; + // x.* ∩ *.y » x.y + // x.*.* ∩ *.y » x.y.* + // x.*.z ∩ *.y » x.y.z + // x.y ∩ *.b » (n/a) + // x.y ∩ a.* » (n/a) + for (let i = 0; i < len; i++) { + a = notesA[i]; + b = notesB[i]; + if (a === b) { + notesI.push(a); + } else if (a && re.WILDCARD.test(a)) { + if (!b) { + notesI.push(a); + } else { + notesI.push(b); + } + } else if (b && re.WILDCARD.test(b)) { + if (!a) { + notesI.push(b); + } else { + notesI.push(a); + } + } else if (a && !b) { + notesI.push(a); + } else if (!a && b) { + notesI.push(b); + } else { + // if (a !== b) { + notesI = []; + break; + } + } + + if (notesI.length > 0) return bang + utils.joinNotes(notesI); + return null; + } + + /** + * Undocumented. + * @private + * + * @param glob - + */ + private static _inspect(glob: string): INotationGlobInspection { + let g = glob.trim(); + if (!NotationGlob.isValid(g)) { + throw new NotationError(`${ERR_INVALID} '${glob}'`); + } + + const isNegated = g[0] === '!'; + // trailing wildcards are only redundant if not negated + if (!isNegated) g = utils.removeTrailingWildcards(g); + const absGlob = isNegated ? g.slice(1) : g; + + return { + glob: g, + absGlob, + isNegated, + // e.g. [*] or [1] are array globs. ["1"] is not. + isArrayGlob: /^\[[^'"]/.test(absGlob) + }; + } + + // when we remove items from an array (via e.g. filtering), we first need to + // remove the item with the greater index so indexes of other items (that are to + // be removed from the same array) do not shift. so below is for comparing 2 + // globs if they represent 2 items from the same array. + + // example items from same array: ![*][2] ![0][*] ![0][1] ![0][3] + // should be sorted as ![0][3] ![*][2] ![0][1] ![0][*] + private static _compareArrayItemGlobs(a: NotationGlob, b: NotationGlob): number { + const reANote = re.ARRAY_GLOB_NOTE; + // both should be negated + if ( + !a.isNegated || + !b.isNegated || + // should be same length (since we're comparing for items in same + // array) + a.notes.length !== b.notes.length || + // last notes should be array brackets + !reANote.test(a.last) || + !reANote.test(b.last) || + // last notes should be different to compare + a.last === b.last + ) + return 0; + + // negated !..[*] should come last + if (a.last === '[*]') return 1; // b is first + if (b.last === '[*]') return -1; // a is first + + if (a.parent && b.parent) { + const { _covers } = NotationGlob; + if (_covers(a.parent, b.parent, true)) { + return _compArrIdx(a.last, b.last); + } + return 0; + } + return _compArrIdx(a.last, b.last); + } +} + +// -------------------------------- +// HELPERS +// -------------------------------- + +// used by _compareArrayItemGlobs() for getting a numeric index from array note. +// we'll use these indexes to sort higher to lower, as removing order; to +// prevent shifted indexes. +function _idxVal(note: string): number { + // we return -1 for wildcard bec. we need it to come last + + // below will never execute when called from _compareArrayItemGlobs + /* istanbul ignore next */ + // if (note === '[*]') return -1; + + // e.g. '[2]' » 2 + return parseInt(note.replace(/[[\]]/, ''), 10); +} + +function _compArrIdx(lastA: string, lastB: string): number { + const iA = _idxVal(lastA); + const iB = _idxVal(lastB); + + // below will never execute when called from _compareArrayItemGlobs + /* istanbul ignore next */ + // if (iA === iB) return 0; + + return iA > iB ? -1 : 1; +} + +// x vs !x.*.* » false +// x vs !x[*] » true +// x[*] vs !x » true +// x[*] vs !x[*] » false +// x.* vs !x.* » false +function _isReverseOf( + a: NotationGlob | INotationGlobInspection, + b: NotationGlob | INotationGlobInspection +): boolean { + return a.isNegated !== b.isNegated && a.absGlob === b.absGlob; +} + +function _invert(glob: string): string { + return glob[0] === '!' ? glob.slice(1) : '!' + glob; +} + +const _rx = /^\s*!/; +function _negFirstSort(a: string, b: string): number { + const negA = _rx.test(a); + const negB = _rx.test(b); + if (negA && negB) return a.length >= b.length ? 1 : -1; + if (negA) return -1; + if (negB) return 1; + return 0; +} +function _negLastSort(a: string, b: string): number { + const negA = _rx.test(a); + const negB = _rx.test(b); + if (negA && negB) return a.length >= b.length ? 1 : -1; + if (negA) return 1; + if (negB) return -1; + return 0; +} From d2675239ca0a16387d84a737ee23ae6f311f3661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20Y=C4=B1ld=C4=B1r=C4=B1m?= Date: Mon, 15 Jun 2026 20:22:23 +0300 Subject: [PATCH 14/17] updated interfaces --- src/core/INotationFilterOptions.ts | 12 ++++++++ src/core/INotationGlobInspection.ts | 23 +++++++++++++++ src/core/INotationInspection.ts | 44 +++++++++++++++++++++++++++++ src/core/INotationOptions.ts | 27 ++++++++++++++++++ 4 files changed, 106 insertions(+) create mode 100644 src/core/INotationFilterOptions.ts create mode 100644 src/core/INotationGlobInspection.ts create mode 100644 src/core/INotationInspection.ts create mode 100644 src/core/INotationOptions.ts diff --git a/src/core/INotationFilterOptions.ts b/src/core/INotationFilterOptions.ts new file mode 100644 index 0000000..1c0cd4e --- /dev/null +++ b/src/core/INotationFilterOptions.ts @@ -0,0 +1,12 @@ +/** + * Represents `Notation` filter options. + */ +export interface INotationFilterOptions { + /** + * Whether negated items strictly remove every match. Note that, regardless + * of this option, if any item has an exact negated version; non-negated is + * always removed. + * @default false + */ + restrictive?: boolean; +} diff --git a/src/core/INotationGlobInspection.ts b/src/core/INotationGlobInspection.ts new file mode 100644 index 0000000..7af82f6 --- /dev/null +++ b/src/core/INotationGlobInspection.ts @@ -0,0 +1,23 @@ +/** + * Represents the notation glob inspection result object. + */ +export interface INotationGlobInspection { + /** + * Original glob notation. + */ + glob: string; + /** + * Absolute (non-negated) version of the original glob notation. + */ + absGlob: string; + /** + * Indicates whether the original glob notation is negated with a `!` + * prefix. + */ + isNegated: boolean; + /** + * Indicates whether it's an array glog. + * e.g. `[*]` or `[1]` are array globs. `["1"]` is not. + */ + isArrayGlob: boolean; +} diff --git a/src/core/INotationInspection.ts b/src/core/INotationInspection.ts new file mode 100644 index 0000000..207a20b --- /dev/null +++ b/src/core/INotationInspection.ts @@ -0,0 +1,44 @@ +/** + * Represents the notation inspection result object. + */ +export interface INotationInspection { + /** + * Notation that is inspected. + */ + notation: string; + /** + * Whether the source object has the given notation as a (leveled) + * enumerable property. If the property exists but has a value of + * `undefined`, this will still return `true`. + */ + has: boolean; + /** + * The value of the notated property. If the source object does not have + * the notation, the value will be `undefined`. + */ + value?: unknown; + /** + * he type of the notated property. If the source object does not have the + * notation, the type will be `"undefined"`. + */ + type: string; + /** + * Level index of the notated value. + */ + level: number; + /** + * Last note of the notation, if actually exists. For example, last note of + * `'a.b.c'` is `'c'`. + */ + lastNote: string; + /** + * Normalized representation of the last note of the notation, if actually + * exists. For example, last note of `'a.b[1]` is `'[1]'` and will be + * normalized to number `1`; which indicates an array index. + */ + lastNoteNormalized: string | number; + /** + * Whether the parent object of the notation path is an array. + */ + parentIsArray: boolean; +} diff --git a/src/core/INotationOptions.ts b/src/core/INotationOptions.ts new file mode 100644 index 0000000..fb761c6 --- /dev/null +++ b/src/core/INotationOptions.ts @@ -0,0 +1,27 @@ +/** + * Represents `Notation` options. + */ +export interface INotationOptions { + /** + * Whether to throw either when a notation path does not exist on the + * source (i.e. `#get()` and `#remove()` methods); or notation path exists + * but overwriting is disabled (i.e. `#set()` method). (Note that + * `.inspectGet()` and `.inspectRemove()` methods are exceptions). It's + * recommended to set this to `true` and prevent silent failures if you're + * working with sensitive data. Regardless of `strict` option, it will + * always throw on invalid notation syntax or other crucial failures. + * @default false + */ + strict?: boolean; + /** + * Whether to preserve indices when an item is REMOVED from an array source. + * This will produce sparse-arrays if enabled. + * @default false + */ + preserveIndices?: boolean; +} + +export const DEFAULT_NOTATION_OPTIONS: INotationOptions = Object.freeze({ + strict: false, + preserveIndices: false +}); From c09e27a32032490a4f3a28ae9e7eda3bfabf8a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20Y=C4=B1ld=C4=B1r=C4=B1m?= Date: Mon, 15 Jun 2026 20:22:39 +0300 Subject: [PATCH 15/17] updated utils. breaking changes! --- src/utils.ts | 249 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 src/utils.ts diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..0bf477e --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,249 @@ +import { NotationError } from './core/NotationError.js'; +import type { + ArrayEachCallbackEE, + Collection, + ObjectEachCallbackEE, + UnknownObject +} from './types.js'; + +const objProto = Object.prototype; +// Stryker disable next-line all: Symbol is always available in supported envs (defensive). +const symValueOf = + typeof Symbol === 'function' ? Symbol.prototype.valueOf : /* istanbul ignore next */ null; + +// never use 'g' (global) flag in regexps below +const VAR = /^[a-z$_][a-z$_\d]*$/i; +const ARRAY_NOTE = /^\[(\d+)\]$/; +const ARRAY_GLOB_NOTE = /^\[(\d+|\*)\]$/; +const OBJECT_BRACKETS = /^\[(?:'(.*)'|"(.*)"|`(.*)`)\]$/; +const WILDCARD = /^(\[\*\]|\*)$/; +// matches `*` and `[*]` if outside of quotes. +const WILDCARDS = /(\*|\[\*\])(?=(?:[^"]|"[^"]*")*$)(?=(?:[^']|'[^']*')*$)/; +// matches trailing wildcards at the end of a non-negated glob. +// e.g. `x.y.*[*].*` » $1 = `x.y`, $2 = `.*[*].*` +const NON_NEG_WILDCARD_TRAIL = /^(?!!)(.+?)(\.\*|\[\*\])+$/; +const NEGATE_ALL = /^!(\*|\[\*\])$/; +// ending with '.*' or '[*]' + +// const _reFlags = /\w*$/; + +export const utils = { + re: { + VAR, + ARRAY_NOTE, + ARRAY_GLOB_NOTE, + OBJECT_BRACKETS, + WILDCARD, + WILDCARDS, + NON_NEG_WILDCARD_TRAIL, + NEGATE_ALL + }, + + type: (o: unknown): string => { + const t = objProto.toString.call(o).match(/\s(\w+)/i) as [string, string]; + return t[1].toLowerCase(); + }, + + isCollection: (o: unknown): o is Collection => { + const t = utils.type(o); + return t === 'object' || t === 'array'; + }, + + isset: (o: unknown): boolean => o !== undefined && o !== null, + + ensureArray: (o: T | T[]): T[] => { + if (Array.isArray(o)) return o; + return o === null || o === undefined ? [] : [o]; + }, + + // simply returning true will get rid of the "holes" in the array. + // e.g. [0, , 1, , undefined, , , 2, , , null].filter(() => true); + // ——» [0, 1, undefined, 2, null] + + cleanSparseArray: (a: unknown[]): unknown[] => { + return a.filter((e) => e !== undefined); // or Object.values(a); + }, + + // added _collectionType for optimization (in loops) + hasOwn: ( + collection: Collection, + keyOrIndex: string | number, + _collectionType?: string + ): boolean => { + if (!collection) return false; + + const isArr = (_collectionType || utils.type(collection)) === 'array'; + + // check for objects + if (!isArr && typeof keyOrIndex === 'string') { + return Boolean(keyOrIndex) && objProto.hasOwnProperty.call(collection, keyOrIndex); + } + + // check for arrays + if (typeof keyOrIndex === 'number') { + return keyOrIndex >= 0 && keyOrIndex < collection.length; + } + + return false; + }, + + _cloneDeep: (collection: unknown): unknown => { + const t = utils.type(collection); + + switch (t) { + case 'date': + return new Date((collection as Date).valueOf()); + + case 'regexp': { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const re = collection as RegExp; + const copy = new RegExp(re.source, re.flags); + copy.lastIndex = re.lastIndex; + return copy; + } + case 'symbol': { + // symValueOf is always defined in supported envs; the fallback is defensive. + /* istanbul ignore next */ + // Stryker disable next-line all: defensive non-Symbol fallback, unreachable here. + if (!symValueOf) return collection; + return Object(symValueOf.call(collection)); + } + + case 'array': + return (collection as unknown[]).map(utils._cloneDeep); + + case 'object': { + const copy = {}; + // only enumerable string keys + Object.keys(collection as UnknownObject).forEach((k) => { + copy[k] = utils._cloneDeep((collection as UnknownObject)[k]); + }); + return copy; + } + + // primitives copied over by value + // case 'string': + // case 'number': + // case 'boolean': + // case 'null': + // case 'undefined': + default: // others will be referenced + return collection; + } + }, + cloneDeep: (collection: T): T => utils._cloneDeep(collection) as T, + + // iterates over elements of an array, executing the callback for each + // element. + each: (array: T[], callback: ArrayEachCallbackEE, context?: unknown): void => { + const len = array.length; + let index = -1; + while (++index < len) { + if (callback.apply(context, [array[index], index, array]) === false) return; + } + }, + + eachRight: ( + array: T[], + callback: ArrayEachCallbackEE, + context?: unknown + ): void => { + let index = array.length; + while (index--) { + if (callback.apply(context, [array[index], index, array]) === false) return; + } + }, + + eachProp: ( + object: T, + callback: ObjectEachCallbackEE, + context?: unknown + ): void => { + const keys = Object.keys(object); + let index = -1; + while (++index < keys.length) { + const key = keys[index]; + if (callback.apply(context, [object[key], key, object]) === false) return; + } + }, + + eachItem: ( + collection: O | A[], + callback: ObjectEachCallbackEE | ArrayEachCallbackEE, + context?: unknown, + reverseIfArray: boolean = false + ): void => { + if (Array.isArray(collection)) { + // important! we should iterate with eachRight to prevent shifted + // indexes when removing items from arrays. + return reverseIfArray + ? utils.eachRight(collection as A[], callback as ArrayEachCallbackEE, context) + : utils.each(collection as A[], callback as ArrayEachCallbackEE, context); + } + + return utils.eachProp(collection, callback as ObjectEachCallbackEE, context); + }, + + pregQuote: (str: string): string => { + const re = /[.\\+*?[^\]$(){}=!<>|:-]/g; + return String(str).replace(re, '\\$&'); + }, + + stringOrArrayOf: (o: unknown, value: string): boolean => + typeof value === 'string' && + (o === value || (Array.isArray(o) && o.length === 1 && o[0] === value)), + + hasSingleItemOf: (arr: string[], ...rest: unknown[]): boolean => + arr.length === 1 && (rest.length === 1 ? arr[0] === rest[0] : true), + + // remove trailing/redundant wildcards if not negated + removeTrailingWildcards: (glob: string): string => + // glob.replace(/(.+?)(\.\*|\[\*\])*$/, '$1') + glob.replace(NON_NEG_WILDCARD_TRAIL, '$1'), + + normalizeNote: (note: string): string | number => { + if (VAR.test(note)) return note; + + // check array index notation e.g. `[1]` + let m = note.match(ARRAY_NOTE); + if (m) return parseInt(m[1], 10); + + // check object bracket notation e.g. `["a-b"]` + m = note.match(OBJECT_BRACKETS); + if (m) return m[1] || m[2] || m[3]; + + throw new NotationError(`Invalid note: '${note}'`); + }, + + // we have NotationGlob.join() but it checks each note item for validity. + // this is used internally to re-join previously splitted without any checks + // so it's faster. + joinNotes: (notes: (string | null | undefined)[]): string => { + const lastIndex = notes.length - 1; + + return notes + .map((current, i) => { + if (!current) return ''; + const next = lastIndex >= i + 1 ? notes[i + 1] : null; + const dot = next ? (next[0] === '[' ? '' : '.') : ''; + return current + dot; + }) + .join(''); + }, + + getNewNotation: (newNotation?: string | null, notation?: string | null): string | never => { + const errMsg = `Invalid new notation: '${newNotation}'`; + + // note validations (for newNotation and notation) are already made by + // other methods in the flow. + let newN; + if (typeof newNotation === 'string') { + newN = newNotation.trim(); + if (!newN) throw new NotationError(errMsg); + return newN; + } + if (notation && !utils.isset(newNotation)) return notation; + + throw new NotationError(errMsg); + } +}; From bb5d5dda990009ecf7d982aba65d19f4ff9f0461 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20Y=C4=B1ld=C4=B1r=C4=B1m?= Date: Mon, 15 Jun 2026 20:22:51 +0300 Subject: [PATCH 16/17] updated --- src/index.ts | 3 +++ src/types.ts | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 src/index.ts create mode 100644 src/types.ts diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..4e3d7db --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3 @@ +export * from './core/Notation.js'; +export * from './core/NotationError.js'; +export * from './core/NotationGlob.js'; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..a7dcf9a --- /dev/null +++ b/src/types.ts @@ -0,0 +1,17 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type UnknownObject = Record; + +export type Collection = UnknownObject | unknown[]; + +/** + * Early-Exit callback (return false for exiting the iteration early). + */ +export type ArrayEachCallbackEE = (item: T, index: number, array: T[]) => void | false; +/** + * Early-Exit callback (return false for exiting the iteration early). + */ +export type ObjectEachCallbackEE = ( + value: T, + key: string, + object: Record +) => void | false; From 35555a4ad40b5268d00291eb0079c07285e7ffb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20Y=C4=B1ld=C4=B1r=C4=B1m?= Date: Mon, 15 Jun 2026 20:25:06 +0300 Subject: [PATCH 17/17] Update release date for version 3.0.0 --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4cf8f9..9921e38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org). -## 3.0.0 (2024-07-04) +## 3.0.0 (2026-06-15) ### Changed - **Breaking**: ESM now. @@ -139,4 +139,4 @@ This is a big major release with lots of **improvements** and some **breaking ch ## 1.0.0 (2016-04-10) ### Added -- initial release. \ No newline at end of file +- initial release.