From 3e59f0c954a9f497190d0a5f3f18a757e810bf00 Mon Sep 17 00:00:00 2001 From: Sam Tolmay Date: Mon, 5 Oct 2020 16:50:02 +0200 Subject: [PATCH] feat: add get package --- .pnp.js | 31 ++ packages/get/.babelrc | 12 + packages/get/jest.config.js | 19 ++ packages/get/package.json | 33 ++ packages/get/src/get.js | 131 ++++++++ packages/get/test/get.test.js | 615 ++++++++++++++++++++++++++++++++++ yarn.lock | 28 +- 7 files changed, 867 insertions(+), 2 deletions(-) create mode 100644 packages/get/.babelrc create mode 100644 packages/get/jest.config.js create mode 100644 packages/get/package.json create mode 100644 packages/get/src/get.js create mode 100644 packages/get/test/get.test.js diff --git a/.pnp.js b/.pnp.js index 8133f3f7a..caebe8fae 100755 --- a/.pnp.js +++ b/.pnp.js @@ -34,6 +34,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "name": "@lowdefy/poc-express", "reference": "workspace:packages/express" }, + { + "name": "@lowdefy/get", + "reference": "workspace:packages/get" + }, { "name": "@lowdefy/graphql", "reference": "workspace:packages/graphql" @@ -47,6 +51,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "ignorePatternData": "(^(?:\\.yarn\\/sdks(?:\\/(?!\\.)(?:(?:(?!(?:^|\\/)\\.).)*?)|$))$)", "fallbackExclusionList": [ ["@lowdefy/engine", ["workspace:packages/engine"]], + ["@lowdefy/get", ["workspace:packages/get"]], ["@lowdefy/graphql", ["workspace:packages/graphql"]], ["@lowdefy/poc", ["workspace:."]], ["@lowdefy/poc-express", ["workspace:packages/express"]], @@ -4124,6 +4129,32 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "SOFT", }] ]], + ["@lowdefy/get", [ + ["workspace:packages/get", { + "packageLocation": "./packages/get/", + "packageDependencies": [ + ["@lowdefy/get", "workspace:packages/get"], + ["@babel/cli", "virtual:7fa6405098723f150ab741c1e73c906de11a676b4cc641bac8b3397ea2dd6efbb913e72a780932220533241b442cc586b41b26c7b5ac786de486992cd2db054c#npm:7.8.4"], + ["@babel/compat-data", "npm:7.9.6"], + ["@babel/core", "npm:7.9.6"], + ["@babel/preset-env", "virtual:7fa6405098723f150ab741c1e73c906de11a676b4cc641bac8b3397ea2dd6efbb913e72a780932220533241b442cc586b41b26c7b5ac786de486992cd2db054c#npm:7.9.6"], + ["@lowdefy/type", "workspace:packages/type"], + ["babel-jest", "virtual:73f25cc0d3f57943fa9b1d737e4809af7a52a784e0ac5fed74b4e1e083308ab7ae2fd45a5424a8bc7ff7caab067690c9357630d657cbd636d6037acc1557fdc2#npm:24.9.0"], + ["eslint", "npm:6.8.0"], + ["eslint-config-airbnb", "virtual:73f25cc0d3f57943fa9b1d737e4809af7a52a784e0ac5fed74b4e1e083308ab7ae2fd45a5424a8bc7ff7caab067690c9357630d657cbd636d6037acc1557fdc2#npm:18.2.0"], + ["eslint-config-prettier", "virtual:73f25cc0d3f57943fa9b1d737e4809af7a52a784e0ac5fed74b4e1e083308ab7ae2fd45a5424a8bc7ff7caab067690c9357630d657cbd636d6037acc1557fdc2#npm:6.12.0"], + ["eslint-plugin-import", "virtual:73f25cc0d3f57943fa9b1d737e4809af7a52a784e0ac5fed74b4e1e083308ab7ae2fd45a5424a8bc7ff7caab067690c9357630d657cbd636d6037acc1557fdc2#npm:2.22.1"], + ["eslint-plugin-jsx-a11y", "virtual:73f25cc0d3f57943fa9b1d737e4809af7a52a784e0ac5fed74b4e1e083308ab7ae2fd45a5424a8bc7ff7caab067690c9357630d657cbd636d6037acc1557fdc2#npm:6.3.1"], + ["eslint-plugin-prettier", "virtual:73f25cc0d3f57943fa9b1d737e4809af7a52a784e0ac5fed74b4e1e083308ab7ae2fd45a5424a8bc7ff7caab067690c9357630d657cbd636d6037acc1557fdc2#npm:3.1.4"], + ["eslint-plugin-react", "virtual:73f25cc0d3f57943fa9b1d737e4809af7a52a784e0ac5fed74b4e1e083308ab7ae2fd45a5424a8bc7ff7caab067690c9357630d657cbd636d6037acc1557fdc2#npm:7.21.2"], + ["eslint-plugin-react-hooks", "virtual:73f25cc0d3f57943fa9b1d737e4809af7a52a784e0ac5fed74b4e1e083308ab7ae2fd45a5424a8bc7ff7caab067690c9357630d657cbd636d6037acc1557fdc2#npm:4.1.2"], + ["jest", "npm:24.9.0"], + ["jest-diff", "npm:24.9.0"], + ["prettier", "npm:2.1.2"] + ], + "linkType": "SOFT", + }] + ]], ["@lowdefy/graphql", [ ["workspace:packages/graphql", { "packageLocation": "./packages/graphql/", diff --git a/packages/get/.babelrc b/packages/get/.babelrc new file mode 100644 index 000000000..2d91b3635 --- /dev/null +++ b/packages/get/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "node": "10" + } + } + ] + ] +} diff --git a/packages/get/jest.config.js b/packages/get/jest.config.js new file mode 100644 index 000000000..b3db30fb9 --- /dev/null +++ b/packages/get/jest.config.js @@ -0,0 +1,19 @@ +module.exports = { + // Automatically clear mock calls and instances between every test + clearMocks: true, + + // The directory where Jest should output its coverage files + coverageDirectory: 'coverage', + + // An array of regexp pattern strings used to skip coverage collection + coveragePathIgnorePatterns: ['/node_modules/', '/dist/'], + + // A list of reporter names that Jest uses when writing coverage reports + coverageReporters: ['text'], + + // The test environment that will be used for testing + testEnvironment: 'node', + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + testPathIgnorePatterns: ['/node_modules/', '/dist/'], +}; diff --git a/packages/get/package.json b/packages/get/package.json new file mode 100644 index 000000000..93e321a31 --- /dev/null +++ b/packages/get/package.json @@ -0,0 +1,33 @@ +{ + "name": "@lowdefy/get", + "version": "1.0.1", + "license": "MIT", + "main": "dist/get.js", + "scripts": { + "build": "babel src --out-dir dist", + "test": "jest --coverage", + "prepare": "yarn build", + "prepublishOnly": "yarn build" + }, + "dependencies": { + "@lowdefy/type": "1.0.1" + }, + "devDependencies": { + "@babel/cli": "7.8.4", + "@babel/compat-data": "7.9.6", + "@babel/core": "7.9.6", + "@babel/preset-env": "7.9.6", + "babel-jest": "24.9.0", + "eslint": "6.8.0", + "eslint-config-airbnb": "18.2.0", + "eslint-config-prettier": "6.12.0", + "eslint-plugin-import": "2.22.1", + "eslint-plugin-jsx-a11y": "6.3.1", + "eslint-plugin-prettier": "3.1.4", + "eslint-plugin-react": "7.21.2", + "eslint-plugin-react-hooks": "4.1.2", + "jest": "24.9.0", + "jest-diff": "24.9.0", + "prettier": "2.1.2" + } +} diff --git a/packages/get/src/get.js b/packages/get/src/get.js new file mode 100644 index 000000000..3462195dd --- /dev/null +++ b/packages/get/src/get.js @@ -0,0 +1,131 @@ +/* eslint-disable no-param-reassign */ +// Source: +// https://github.com/jonschlinkert/get-value/blob/master/index.js +// https://www.npmjs.com/package/get-value + +// The MIT License (MIT) + +// Copyright (c) 2014-2018, Jon Schlinkert. + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import typeTest from '@lowdefy/type'; + +function join(segs, joinChar, options) { + if (typeof options.join === 'function') { + return options.join(segs); + } + return segs[0] + joinChar + segs[1]; +} + +function split(path, splitChar, options) { + if (typeof options.split === 'function') { + return options.split(path); + } + return path.split(splitChar); +} + +function isValid(key, target, options) { + if (typeof options.isValid === 'function') { + return options.isValid(key, target); + } + return true; +} + +function isValidObject(val) { + return typeTest.isObject(val) || Array.isArray(val) || typeof val === 'function'; +} + +function get(target, path, options) { + if (!typeTest.isObject(options)) { + options = { default: options }; + } + + if (typeTest.isNone(path) || !isValidObject(target)) { + return typeof options.default !== 'undefined' ? options.default : undefined; + } + + if (typeof path === 'number') { + path = String(path); + } + + const isArray = Array.isArray(path); + const isString = typeof path === 'string'; + const splitChar = options.separator || '.'; + const joinChar = options.joinChar || (typeof splitChar === 'string' ? splitChar : '.'); + + if (isString && path in target) { + return isValid(path, target, options) ? target[path] : options.default; + } + + const segs = isArray ? path : split(path, splitChar, options); + const len = segs.length; + let idx = 0; + + do { + let prop = segs[idx]; + if (typeof prop === 'number') { + prop = String(prop); + } + + while (prop && prop.slice(-1) === '\\') { + idx += 1; + prop = join([prop.slice(0, -1), segs[idx] || ''], joinChar, options); + } + + if (prop in target) { + if (!isValid(prop, target, options)) { + return options.default; + } + + target = target[prop]; + } else { + let hasProp = false; + let n = idx + 1; + + while (n < len) { + prop = join([prop, segs[n]], joinChar, options); + n += 1; + hasProp = prop in target; + if (hasProp) { + if (!isValid(prop, target, options)) { + return options.default; + } + + target = target[prop]; + idx = n - 1; + break; + } + } + + if (!hasProp) { + return options.default; + } + } + // eslint-disable-next-line no-plusplus + } while (++idx < len && isValidObject(target)); + + if (idx === len) { + return target; + } + + return options.default; +} + +export default get; diff --git a/packages/get/test/get.test.js b/packages/get/test/get.test.js new file mode 100644 index 000000000..7ba8968ec --- /dev/null +++ b/packages/get/test/get.test.js @@ -0,0 +1,615 @@ +import get from '../src/get'; + +const diff = require('jest-diff'); + +const expectToEqual = (result, value) => { + expect(result).toEqual(value); +}; + +const expectToStrictEqual = (result, value) => { + expect(result).toStrictEqual(value); +}; + +expect.extend({ + deepStrictEqual(received, expected) { + const options = {}; + const pass = this.equals(received, expected); + const message = pass + ? () => + `${this.utils.matcherHint('deepStrictEqual', undefined, undefined, options)}\n\n` + + `Expected: ${this.utils.printExpected(expected)}\n` + + `Received: ${this.utils.printReceived(received)}` + : () => { + const diffString = diff(expected, received, { + expand: this.expand, + }); + return `${this.utils.matcherHint('deepStrictEqual', undefined, undefined, options)}\n\n${ + diffString && diffString.includes('- Expect') + ? `Difference:\n\n${diffString}` + : `Expected: ${this.utils.printExpected( + expected + )}\nReceived: ${this.utils.printReceived(received)}` + }`; + }; + + return { actual: received, message, pass }; + }, +}); + +const expectDeepStrictEqual = (result, value) => { + expect(result).deepStrictEqual(value); +}; + +test('get a', () => { + const objOne = { + a: 1, + }; + expect(get(objOne, 'a')).toEqual(1); +}); + +test('get undefined', () => { + const objOne = { + a: 1, + }; + expect(get(objOne, 'b')).toEqual(undefined); +}); + +test('get a.b', () => { + const objOne = { + a: { b: 1 }, + }; + expect(get(objOne, 'a.b')).toEqual(1); +}); + +test('get --b', () => { + const objOne = { + '--b': 10, + }; + expect(get(objOne, '--b')).toEqual(10); +}); + +test('get a.--b', () => { + const objOne = { + a: { '--b': 1 }, + }; + expect(get(objOne, 'a.--b')).toEqual(1); +}); + +test('get a.undefined', () => { + const objOne = { + a: { b: 1 }, + }; + expect(get(objOne, 'a.c')).toEqual(undefined); +}); + +test('get a.0.b in array', () => { + const objOne = { + a: [{ b: 1 }, { c: 2 }], + }; + expect(get(objOne, 'a.1.c')).toEqual(2); +}); + +test('return alt value', () => { + const objOne = { + a: 1, + }; + expect(get(objOne, 'b', 3)).toEqual(3); +}); + +test('return null value', () => { + const objOne = { + a: null, + }; + expect(get(objOne, 'a', 2)).toEqual(null); +}); + +test('return arr', () => { + const objOne = { + a: { b: [] }, + }; + expect(get(objOne, 'a.b')).toEqual([]); +}); + +// tests from +// https://github.com/jonschlinkert/get-value/blob/master/test/units.js + +test('should return undefined when non-object given as the first argument', () => { + expect(get(null)).toEqual(undefined); + expect(get('foo')).toEqual(undefined); + expect(get(['a'])).toEqual(undefined); + expect(get({ a: 'a' })).toEqual(undefined); +}); + +test('should return default value when non-object given as the first argument with default value', () => { + expect(get(null, undefined, { default: 'default' })).toEqual('default'); + expect(get('foo', undefined, { default: 'default' })).toEqual('default'); + expect(get(['a'], undefined, { default: 'default' })).toEqual('default'); + expect(get({ a: 'a' }, undefined, { default: 'default' })).toEqual('default'); +}); + +test('should get a value', () => { + expectToEqual(get({ a: 'a', b: { c: 'd' } }, 'a'), 'a'); + expectToEqual(get({ a: 'a', b: { c: 'd' } }, 'b.c'), 'd'); + expectToEqual(get({ foo: 'bar' }, 'foo.bar'), undefined); +}); + +test('should get a property that has dots in the key', () => { + expectToEqual(get({ 'a.b': 'c' }, 'a.b'), 'c'); +}); + +test('should support using dot notation to get nested values', () => { + const fixture = { + a: { locals: { name: { first: 'Brian' } } }, + b: { locals: { name: { last: 'Woodward' } } }, + c: { locals: { paths: ['a.txt', 'b.js', 'c.hbs'] } }, + }; + expectDeepStrictEqual(get(fixture, 'a.locals.name'), { first: 'Brian' }); + expectDeepStrictEqual(get(fixture, 'b.locals.name'), { last: 'Woodward' }); + expectToStrictEqual(get(fixture, 'b.locals.name.last'), 'Woodward'); + expectToStrictEqual(get(fixture, 'c.locals.paths.0'), 'a.txt'); + expectToStrictEqual(get(fixture, 'c.locals.paths.1'), 'b.js'); + expectToStrictEqual(get(fixture, 'c.locals.paths.2'), 'c.hbs'); + expectToStrictEqual(get(fixture, 'c.locals.paths.0'), 'a.txt'); + expectToStrictEqual(get(fixture, 'c.locals.paths.1'), 'b.js'); + expectToStrictEqual(get(fixture, 'c.locals.paths.2'), 'c.hbs'); +}); + +test('should support a custom separator on options.separator', () => { + const fixture = { 'a.b': { c: { d: 'e' } } }; + expectToStrictEqual(get(fixture, 'a.b/c/d', { separator: '/' }), 'e'); + expectToStrictEqual(get(fixture, 'a\\.b.c.d', { separator: /\\?\./ }), 'e'); +}); + +test('should support a custom split function', () => { + const fixture = { 'a.b': { c: { d: 'e' } } }; + expectToStrictEqual(get(fixture, 'a.b/c/d', { split: (path) => path.split('/') }), 'e'); + expectToStrictEqual(get(fixture, 'a\\.b.c.d', { split: (path) => path.split(/\\?\./) }), 'e'); +}); + +test('should support a custom join character', () => { + const fixture = { 'a-b': { c: { d: 'e' } } }; + const options = { joinChar: '-' }; + expectToStrictEqual(get(fixture, 'a.b.c.d', options), 'e'); +}); + +test('should support a custom join function', () => { + const fixture = { 'a-b': { c: { d: 'e' } } }; + const options = { + split: (path) => path.split(/[-/]/), + join: (segs) => segs.join('-'), + }; + expectToStrictEqual(get(fixture, 'a/b-c/d', options), 'e'); +}); + +test('should support a default value as the last argument', () => { + const fixture = { foo: { c: { d: 'e' } } }; + expectToEqual(get(fixture, 'foo.bar.baz', 'quz'), 'quz'); + expectToEqual(get(fixture, 'foo.bar.baz', true), true); + expectToEqual(get(fixture, 'foo.bar.baz', false), false); + expectToEqual(get(fixture, 'foo.bar.baz', null), null); +}); + +test('should support options.default', () => { + const fixture = { foo: { c: { d: 'e' } } }; + expectToEqual(get(fixture, 'foo.bar.baz', { default: 'qux' }), 'qux'); + expectToEqual(get(fixture, 'foo.bar.baz', { default: true }), true); + expectToEqual(get(fixture, 'foo.bar.baz', { default: false }), false); + expectToEqual(get(fixture, 'foo.bar.baz', { default: null }), null); + expectDeepStrictEqual(get(fixture, 'foo.bar.baz', { default: { one: 'two' } }), { one: 'two' }); +}); + +test('should support a custom function for validating the object', () => { + const isEnumerable = Object.prototype.propertyIsEnumerable; + const options = { + isValid(key, obj) { + return isEnumerable.call(obj, key); + }, + }; + + const fixture = { 'a.b': { c: { d: 'e' } } }; + expectToStrictEqual(get(fixture, 'a.b.c.d', options), 'e'); +}); + +test('should support nested keys with dots', () => { + expectToStrictEqual(get({ 'a.b.c': 'd' }, 'a.b.c'), 'd'); + expectToStrictEqual(get({ 'a.b': { c: 'd' } }, 'a.b.c'), 'd'); + expectToStrictEqual(get({ 'a.b': { c: { d: 'e' } } }, 'a.b.c.d'), 'e'); + expectToStrictEqual(get({ a: { b: { c: 'd' } } }, 'a.b.c'), 'd'); + expectToStrictEqual(get({ a: { 'b.c': 'd' } }, 'a.b.c'), 'd'); + expectToStrictEqual(get({ 'a.b.c.d': 'e' }, 'a.b.c.d'), 'e'); + expectToStrictEqual(get({ 'a.b.c.d': 'e' }, 'a.b.c'), undefined); + + expectToStrictEqual(get({ 'a.b.c.d.e.f': 'g' }, 'a.b.c.d.e.f'), 'g'); + expectToStrictEqual(get({ 'a.b.c.d.e': { f: 'g' } }, 'a.b.c.d.e.f'), 'g'); + expectToStrictEqual(get({ 'a.b.c.d': { e: { f: 'g' } } }, 'a.b.c.d.e.f'), 'g'); + expectToStrictEqual(get({ 'a.b.c': { d: { e: { f: 'g' } } } }, 'a.b.c.d.e.f'), 'g'); + expectToStrictEqual(get({ 'a.b': { c: { d: { e: { f: 'g' } } } } }, 'a.b.c.d.e.f'), 'g'); + expectToStrictEqual(get({ a: { b: { c: { d: { e: { f: 'g' } } } } } }, 'a.b.c.d.e.f'), 'g'); + + expectDeepStrictEqual(get({ 'a.b.c.d.e': { f: 'g' } }, 'a.b.c.d.e'), { + f: 'g', + }); + expectDeepStrictEqual(get({ 'a.b.c.d': { 'e.f': 'g' } }, 'a.b.c.d.e'), undefined); + expectDeepStrictEqual(get({ 'a.b.c': { 'd.e.f': 'g' } }, 'a.b.c'), { + 'd.e.f': 'g', + }); + expectDeepStrictEqual(get({ 'a.b': { 'c.d.e.f': 'g' } }, 'a.b'), { + 'c.d.e.f': 'g', + }); + expectDeepStrictEqual(get({ a: { 'b.c.d.e.f': 'g' } }, 'a'), { + 'b.c.d.e.f': 'g', + }); + + expectToStrictEqual(get({ 'a.b.c.d.e': { f: 'g' } }, 'a.b.c.d.e.f'), 'g'); + expectToStrictEqual(get({ 'a.b.c.d': { 'e.f': 'g' } }, 'a.b.c.d.e.f'), 'g'); + expectToStrictEqual(get({ 'a.b.c': { 'd.e.f': 'g' } }, 'a.b.c.d.e.f'), 'g'); + expectToStrictEqual(get({ 'a.b': { 'c.d.e.f': 'g' } }, 'a.b.c.d.e.f'), 'g'); + expectToStrictEqual(get({ a: { 'b.c.d.e.f': 'g' } }, 'a.b.c.d.e.f'), 'g'); + + expectToStrictEqual(get({ 'a.b': { 'c.d': { 'e.f': 'g' } } }, 'a.b.c.d.e.f'), 'g'); + expectToStrictEqual(get({ 'a.b': { c: { 'd.e.f': 'g' } } }, 'a.b.c.d.e.f'), 'g'); + expectToStrictEqual(get({ a: { 'b.c.d.e': { f: 'g' } } }, 'a.b.c.d.e.f'), 'g'); + expectToStrictEqual(get({ a: { 'b.c.d': { 'e.f': 'g' } } }, 'a.b.c.d.e.f'), 'g'); + expectToStrictEqual(get({ a: { 'b.c': { 'd.e.f': 'g' } } }, 'a.b.c.d.e.f'), 'g'); + expectToStrictEqual(get({ a: { b: { 'c.d.e.f': 'g' } } }, 'a.b.c.d.e.f'), 'g'); +}); + +test('should support return default when options.isValid returns false', () => { + const fixture = { foo: { bar: { baz: 'qux' }, 'a.b.c': 'xyx', yyy: 'zzz' } }; + const options = (val) => ({ + ...{ + default: val, + isValid(key) { + return key !== 'bar' && key !== 'a.b.c'; + }, + }, + }); + + expectToEqual(get(fixture, 'foo.bar.baz', options('fez')), 'fez'); + expectToEqual(get(fixture, 'foo.bar.baz', options(true)), true); + expectToEqual(get(fixture, 'foo.bar.baz', options(false)), false); + expectToEqual(get(fixture, 'foo.bar.baz', options(null)), null); + + expectToEqual(get(fixture, 'foo.a.b.c', options('fez')), 'fez'); + expectToEqual(get(fixture, 'foo.a.b.c', options(true)), true); + expectToEqual(get(fixture, 'foo.a.b.c', options(false)), false); + expectToEqual(get(fixture, 'foo.a.b.c', options(null)), null); + + expectToEqual(get(fixture, 'foo.yyy', options('fez')), 'zzz'); +}); + +test('should get a value from an array', () => { + const fixture = { + a: { paths: ['a.txt', 'a.js', 'a.hbs'] }, + b: { + paths: { + 0: 'b.txt', + 1: 'b.js', + 2: 'b.hbs', + 3: 'b3.hbs', + }, + }, + }; + expectToStrictEqual(get(fixture, 'a.paths.0'), 'a.txt'); + expectToStrictEqual(get(fixture, 'a.paths.1'), 'a.js'); + expectToStrictEqual(get(fixture, 'a.paths.2'), 'a.hbs'); + + expectToStrictEqual(get(fixture, 'b.paths.0'), 'b.txt'); + expectToStrictEqual(get(fixture, 'b.paths.1'), 'b.js'); + expectToStrictEqual(get(fixture, 'b.paths.2'), 'b.hbs'); + expectToStrictEqual(get(fixture, 'b.paths.3'), 'b3.hbs'); +}); + +test('should get a value from an object in an array', () => { + expectToStrictEqual(get({ a: { b: [{ c: 'd' }] } }, 'a.b.0.c'), 'd'); + expectToStrictEqual(get({ a: { b: [{ c: 'd' }, { e: 'f' }] } }, 'a.b.1.e'), 'f'); + expectToStrictEqual(get({ a: { b: [{ c: 'd' }] } }, 'a.b.0.c'), 'd'); + expectToStrictEqual(get({ a: { b: [{ c: 'd' }, { e: 'f' }] } }, 'a.b.1.e'), 'f'); +}); + +test('should return `undefined` if the path is not found', () => { + const fixture = { a: { b: {} } }; + expectToStrictEqual(get(fixture, 'a.b.c'), undefined); + expectToStrictEqual(get(fixture, 'a.b.c.d'), undefined); +}); + +test('should get the specified property', () => { + expectDeepStrictEqual(get({ a: 'aaa', b: 'b' }, 'a'), 'aaa'); + expectDeepStrictEqual(get({ first: 'Jon', last: 'Schlinkert' }, 'first'), 'Jon'); + expectDeepStrictEqual(get({ locals: { a: 'a' }, options: { b: 'b' } }, 'locals'), { a: 'a' }); +}); + +test('should support passing a property formatted as an array', () => { + expectDeepStrictEqual(get({ a: 'aaa', b: 'b' }, ['a']), 'aaa'); + expectDeepStrictEqual(get({ a: { b: { c: 'd' } } }, ['a', 'b', 'c']), 'd'); + expectDeepStrictEqual(get({ first: 'Harry', last: 'Potter' }, ['first']), 'Harry'); + expectDeepStrictEqual(get({ locals: { a: 'a' }, options: { b: 'b' } }, ['locals']), { a: 'a' }); +}); + +test('should support escaped dots', () => { + expectDeepStrictEqual(get({ 'a.b': 'a', b: { c: 'd' } }, 'a\\.b'), 'a'); + expectDeepStrictEqual(get({ 'a.b': { b: { c: 'd' } } }, 'a\\.b.b.c'), 'd'); +}); + +test('should get the value of a deeply nested property', () => { + expectToStrictEqual(get({ a: { b: 'c', c: { d: 'e', e: 'f', g: { h: 'i' } } } }, 'a.c.g.h'), 'i'); +}); + +/** + * These tests are from the "dot-prop" library + */ + +describe('dot-prop tests:', () => { + test('should pass dot-prop tests', () => { + const f1 = { foo: { bar: 1 } }; + f1[''] = 'foo'; + expectDeepStrictEqual(get(f1, ''), 'foo'); + expectDeepStrictEqual(get(f1, 'foo'), f1.foo); + expectDeepStrictEqual(get({ foo: 1 }, 'foo'), 1); + expectDeepStrictEqual(get({ foo: null }, 'foo'), null); + expectDeepStrictEqual(get({ foo: undefined }, 'foo'), undefined); + expectDeepStrictEqual(get({ foo: { bar: true } }, 'foo.bar'), true); + expectDeepStrictEqual(get({ foo: { bar: { baz: true } } }, 'foo.bar.baz'), true); + expectDeepStrictEqual(get({ foo: { bar: { baz: null } } }, 'foo.bar.baz'), null); + expectDeepStrictEqual(get({ '\\': true }, '\\'), true); + expectDeepStrictEqual(get({ '\\foo': true }, '\\foo'), true); + expectDeepStrictEqual(get({ 'bar\\': true }, 'bar\\'), true); + expectDeepStrictEqual(get({ 'foo\\bar': true }, 'foo\\bar'), true); + expectDeepStrictEqual(get({ '\\.foo': true }, '\\\\.foo'), true); + expectDeepStrictEqual(get({ 'bar\\.': true }, 'bar\\\\.'), true); + expectDeepStrictEqual(get({ 'foo\\.bar': true }, 'foo\\\\.bar'), true); + expectDeepStrictEqual(get({ foo: 1 }, 'foo.bar'), undefined); + + function fn() {} + fn.foo = { bar: 1 }; + expectDeepStrictEqual(get(fn, 'foo'), fn.foo); + expectDeepStrictEqual(get(fn, 'foo.bar'), 1); + + const f3 = { foo: null }; + expectDeepStrictEqual(get(f3, 'foo.bar'), undefined); + expectDeepStrictEqual(get(f3, 'foo.bar', { default: 'some value' }), 'some value'); + + expectDeepStrictEqual(get({ 'foo.baz': { bar: true } }, 'foo\\.baz.bar'), true); + expectDeepStrictEqual(get({ 'fo.ob.az': { bar: true } }, 'fo\\.ob\\.az.bar'), true); + + expectDeepStrictEqual(get(null, 'foo.bar', { default: false }), false); + expectDeepStrictEqual(get('foo', 'foo.bar', { default: false }), false); + expectDeepStrictEqual(get([], 'foo.bar', { default: false }), false); + expectDeepStrictEqual(get(undefined, 'foo.bar', { default: false }), false); + }); + + test('should use a custom options.isValid function', () => { + const isEnumerable = Object.prototype.propertyIsEnumerable; + const options = { + isValid: (key, obj) => isEnumerable.call(obj, key), + }; + + const target = {}; + Object.defineProperty(target, 'foo', { + value: 'bar', + enumerable: false, + }); + + expectDeepStrictEqual(get(target, 'foo', options), undefined); + expectDeepStrictEqual(get({}, 'hasOwnProperty', options), undefined); + }); + + test('should return a default value', () => { + expectDeepStrictEqual(get({ foo: { bar: 'a' } }, 'foo.fake'), undefined); + expectDeepStrictEqual(get({ foo: { bar: 'a' } }, 'foo.fake.fake2'), undefined); + expectDeepStrictEqual(get({ foo: { bar: 'a' } }, 'foo.fake.fake2', 'some value'), 'some value'); + }); + + test('should pass all of the dot-prop tests', () => { + const f1 = { foo: { bar: 1 } }; + expectDeepStrictEqual(get(f1), undefined); + expectDeepStrictEqual(get(f1, 'foo'), f1.foo); + expectDeepStrictEqual(get({ foo: 1 }, 'foo'), 1); + expectDeepStrictEqual(get({ foo: null }, 'foo'), null); + expectDeepStrictEqual(get({ foo: undefined }, 'foo'), undefined); + expectDeepStrictEqual(get({ foo: { bar: true } }, 'foo.bar'), true); + expectDeepStrictEqual(get({ foo: { bar: { baz: true } } }, 'foo.bar.baz'), true); + expectDeepStrictEqual(get({ foo: { bar: { baz: null } } }, 'foo.bar.baz'), null); + expectDeepStrictEqual(get({ foo: { bar: 'a' } }, 'foo.fake.fake2'), undefined); + }); +}); + +/** + * These tests are from the "object-path" library + */ + +describe('object-path .get tests', () => { + function getTestObj() { + return { + a: 'b', + b: { + c: [], + d: ['a', 'b'], + e: [{}, { f: 'g' }], + f: 'i', + }, + }; + } + + test('should return the value using unicode key', () => { + const obj = { '15\u00f8C': { '3\u0111': 1 } }; + expectToEqual(get(obj, '15\u00f8C.3\u0111'), 1); + expectToEqual(get(obj, ['15\u00f8C', '3\u0111']), 1); + }); + + test('should return the value using dot in key (with array of segments)', () => { + const obj = { 'a.b': { 'looks.like': 1 } }; + expectToEqual(get(obj, ['a.b', 'looks.like']), 1); + }); + + test('should return the value of an empty array', () => { + const obj = { a: { b: [] } }; + expectToEqual(get(obj, 'a.b'), []); + }); + + // object-path fails this test + test('should return the value using dot in key', () => { + const obj = { 'a.b': { 'looks.like': 1 } }; + expectToEqual(get(obj, 'a.b.looks.like'), 1); + }); + + test('should return the value under shallow object', () => { + const obj = getTestObj(); + expectToEqual(get(obj, 'a'), 'b'); + expectToEqual(get(obj, ['a']), 'b'); + }); + + test('should work with number path', () => { + const obj = getTestObj(); + expectToEqual(get(obj.b.d, 0), 'a'); + expectToEqual(get(obj.b, 0), undefined); + }); + + test('should return the value under deep object', () => { + const obj = getTestObj(); + expectToEqual(get(obj, 'b.f'), 'i'); + expectToEqual(get(obj, ['b', 'f']), 'i'); + }); + + test('should return the value under array', () => { + const obj = getTestObj(); + expectToEqual(get(obj, 'b.d.0'), 'a'); + expectToEqual(get(obj, ['b', 'd', 0]), 'a'); + }); + + test('should return the value under array deep', () => { + const obj = getTestObj(); + expectToEqual(get(obj, 'b.e.1.f'), 'g'); + expectToEqual(get(obj, ['b', 'e', 1, 'f']), 'g'); + }); + + test('should return undefined for missing values under object', () => { + const obj = getTestObj(); + expectToEqual(get(obj, 'a.b'), undefined); + expectToEqual(get(obj, ['a', 'b']), undefined); + }); + + test('should return undefined for missing values under array', () => { + const obj = getTestObj(); + expectToEqual(get(obj, 'b.d.5'), undefined); + expectToEqual(get(obj, ['b', 'd', '5']), undefined); + }); + + test('should return the value under integer-like key', () => { + const obj = { '1a': 'foo' }; + expectToEqual(get(obj, '1a'), 'foo'); + expectToEqual(get(obj, ['1a']), 'foo'); + }); + + test('should return the default value when the key doesnt exist', () => { + const obj = { '1a': 'foo' }; + expectToEqual(get(obj, '1b', null), null); + expectToEqual(get(obj, ['1b'], null), null); + }); + + // this test differs from behavior in object-path. I was unable to figure + // out exactly how the default values work in object-path. + test('should return the default value when path is empty', () => { + const obj = { '1a': 'foo' }; + expectDeepStrictEqual(get(obj, '', null), null); + expectDeepStrictEqual(get(obj, []), undefined); + expectToEqual(get({}, ['1'], 'foo'), 'foo'); + }); + + test('should return the default value when object is null or undefined', () => { + expectDeepStrictEqual(get(null, 'test', { default: 'a' }), 'a'); + expectDeepStrictEqual(get(undefined, 'test', { default: 'a' }), 'a'); + }); + + test('should not fail on an object with a null prototype', () => { + const foo = 'FOO'; + const objWithNullProto = Object.create(null); + objWithNullProto.foo = foo; + expectToEqual(get(objWithNullProto, 'foo'), foo); + }); + + // this differs from object-path, which does not allow + // the user to get non-own properties for some reason. + test('should get non-"own" properties', () => { + const Base = function () {}; + Base.prototype = { + one: { + two: true, + }, + }; + const Extended = function () { + Base.call(this, true); + }; + Extended.prototype = Object.create(Base.prototype); + + const extended = new Extended(); + + expectToEqual(get(extended, ['one', 'two']), true); + extended.enabled = true; + + expectToEqual(get(extended, 'enabled'), true); + expectDeepStrictEqual(get(extended, 'one'), { two: true }); + }); +}); + +describe('deep-property unit tests', () => { + test('should handle invalid input', () => { + const a = undefined; + const b = {}; + + expectToEqual(get(a, 'sample'), undefined); + expectDeepStrictEqual(get(b, undefined), undefined); + expectDeepStrictEqual(get(b, ''), undefined); + expectDeepStrictEqual(get(b, '...'), undefined); + }); + + test('should get shallow properties', () => { + const fn = function () {}; + const a = { + sample: 'string', + example: fn, + unknown: undefined, + }; + + expectToEqual(get(a, 'example'), fn); + expectToEqual(get(a, 'sample'), 'string'); + expectToEqual(get(a, 'unknown'), undefined); + expectToEqual(get(a, 'invalid'), undefined); + }); + + test('should get deep properties', () => { + const a = { + b: { example: { type: 'vegetable' } }, + c: { example: { type: 'mineral' } }, + }; + + expectToEqual(get(a, 'b.example.type'), 'vegetable'); + expectToEqual(get(a, 'c.example.type'), 'mineral'); + expectToEqual(get(a, 'c.gorky.type'), undefined); + }); + + // test('should get properties of Error', () => { + // const e = new Error(403); + // console.log(e.message); + // expect(get(e, 'code')).toBe(403); + // }); + + test('should get properties on non-objects', () => { + const fn = function () {}; + + // the commented out lines are from from the "deep-property" lib, + // but it's invalid javascript. This is a good example of why it's always + // better to use "use strict" (and lint your code). + + // const str = 'An example string'; + // const num = 42; + + fn.path = { to: { property: 'string' } }; + // str.path = { to: { property: 'string' } }; + // num.path = { to: { property: 'string' } }; + + expectToEqual(get(fn, 'path.to.property'), 'string'); + // expectToEqual(get(str, 'path.to.property'), undefined); + // expectToEqual(get(num, 'path.to.property'), undefined); + }); +}); diff --git a/yarn.lock b/yarn.lock index ae9846fe5..2daafb951 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2710,6 +2710,30 @@ __metadata: languageName: unknown linkType: soft +"@lowdefy/get@workspace:packages/get": + version: 0.0.0-use.local + resolution: "@lowdefy/get@workspace:packages/get" + dependencies: + "@babel/cli": 7.8.4 + "@babel/compat-data": 7.9.6 + "@babel/core": 7.9.6 + "@babel/preset-env": 7.9.6 + "@lowdefy/type": 1.0.1 + babel-jest: 24.9.0 + eslint: 6.8.0 + eslint-config-airbnb: 18.2.0 + eslint-config-prettier: 6.12.0 + eslint-plugin-import: 2.22.1 + eslint-plugin-jsx-a11y: 6.3.1 + eslint-plugin-prettier: 3.1.4 + eslint-plugin-react: 7.21.2 + eslint-plugin-react-hooks: 4.1.2 + jest: 24.9.0 + jest-diff: 24.9.0 + prettier: 2.1.2 + languageName: unknown + linkType: soft + "@lowdefy/graphql@0.0.0-experimental.0, @lowdefy/graphql@workspace:packages/graphql": version: 0.0.0-use.local resolution: "@lowdefy/graphql@workspace:packages/graphql" @@ -2775,7 +2799,7 @@ __metadata: languageName: unknown linkType: soft -"@lowdefy/type@workspace:packages/type": +"@lowdefy/type@1.0.1, @lowdefy/type@workspace:packages/type": version: 0.0.0-use.local resolution: "@lowdefy/type@workspace:packages/type" dependencies: @@ -10106,7 +10130,7 @@ fsevents@~2.1.2: languageName: node linkType: hard -"jest-diff@npm:^24.9.0": +"jest-diff@npm:24.9.0, jest-diff@npm:^24.9.0": version: 24.9.0 resolution: "jest-diff@npm:24.9.0" dependencies: