Skip to content

Commit

Permalink
feat: add dot notation path setter and getter (#108)
Browse files Browse the repository at this point in the history
Because

- Formik use this kind of path notation to set proper object, we need this kind of function too.
- `dot.setter(obj, "path.to.value", "hi")` will construct `{ path: { to: { value: "hi" }}}`

This commit

- Implement dot.setter and dot.getter
  • Loading branch information
EiffelFly committed Jul 6, 2022
1 parent 2df85f3 commit e31e172
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 6 deletions.
6 changes: 0 additions & 6 deletions .github/workflows/playwright.yml

This file was deleted.

14 changes: 14 additions & 0 deletions src/lib/dot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Context

This lib convert dot notation like `path.to.value` to reference `{ path: { to: value }}`

# Reference

- [Formik - setIn](https://github.com/jaredpalmer/formik/blob/b9cc2536a1edb9f2d69c4cd20ecf4fa0f8059ade/packages/formik/src/utils.ts#L106)
- [Formil - getIn](https://github.com/jaredpalmer/formik/blob/b9cc2536a1edb9f2d69c4cd20ecf4fa0f8059ade/packages/formik/src/utils.ts#L69)
- [Convert a JavaScript string in dot notation into an object reference](https://stackoverflow.com/questions/6393943/convert-a-javascript-string-in-dot-notation-into-an-object-reference)
- [Lodash - BaseSet](https://github.com/lodash/lodash/blob/ddfd9b11a0126db2302cb70ec9973b66baec0975/lodash.js#L3965)

# Caveats

- Currently don't support bracket path `foo[0][1]`, it only support `foo.0.1`
115 changes: 115 additions & 0 deletions src/lib/dot/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import dot from ".";

describe("getter", () => {
const obj = {
foo: {
bar: "yes!",
},
};

it("gets a value by array path", () => {
expect(dot.getter(obj, ["foo", "bar"])).toBe("yes!");
});

it("gets a value by string path", () => {
expect(dot.getter(obj, "foo.bar")).toBe("yes!");
});

it('return "undefined" if value was not found using given path', () => {
expect(dot.getter(obj, "foo.aar")).toBeUndefined();
});

it("return defaultValue if value was not found using given path", () => {
expect(dot.getter(obj, "foo.aar", "no!")).toBe("no!");
});
});

describe("setter", () => {
it("sets flat value", () => {
const obj = { foo: "bar" };
dot.setter(obj, "flat", "value");
expect(obj).toEqual({ foo: "bar", flat: "value" });
});

it("removes flat value", () => {
const obj = { foo: "bar" };
dot.setter(obj, "foo", undefined);
expect(obj).toEqual({});
});

it("sets nested value", () => {
const obj = { x: "y" };
dot.setter(obj, "foo.bar", "hi");
expect(obj).toEqual({ x: "y", foo: { bar: "hi" } });
});

it("updates nested value", () => {
const obj = { x: "y", foo: { bar: "a" } };
dot.setter(obj, "foo.bar", "b");
expect(obj).toEqual({ x: "y", foo: { bar: "b" } });
});

it("removes nested value", () => {
const obj = { x: "y", foo: { bar: "a" } };
dot.setter(obj, "foo.bar", undefined);
expect(obj).toEqual({ x: "y", foo: {} });
expect(obj.foo).not.toHaveProperty("bar");
});

it("updates deep nested value", () => {
const obj = { x: "y", twofoldly: { foo: { bar: "a" } } };
dot.setter(obj, "twofoldly.foo.bar", "b");
expect(obj).toEqual({ x: "y", twofoldly: { foo: { bar: "b" } } });
});

it("removes deep nested value", () => {
const obj = { x: "y", twofoldly: { foo: { bar: "a" } } };
dot.setter(obj, "twofoldly.foo.bar", undefined);
expect(obj).toEqual({ x: "y", twofoldly: { foo: {} } });
expect(obj.twofoldly.foo).not.toHaveProperty("bar");
});

it("sets new array", () => {
const obj = { x: "y" };
dot.setter(obj, "foo.0", "bar");
expect(obj).toEqual({ x: "y", foo: ["bar"] });
});

it("updates nested array value", () => {
const obj = { x: "y", foo: ["bar"] };
dot.setter(obj, "foo.0", "bar");
expect(obj).toEqual({ x: "y", foo: ["bar"] });
});

it("adds new item to nested array", () => {
const obj = { x: "y", foo: ["bar"] };
dot.setter(obj, "foo.1", "bar2");
expect(obj).toEqual({ x: "y", foo: ["bar", "bar2"] });
});

it("sticks to object with int key when defined", () => {
const obj = { x: "y", foo: { 0: "a" } };
dot.setter(obj, "foo.0", "b");
expect(obj).toEqual({ x: "y", foo: { 0: "b" } });
});

// We are currently don't support bracket path

// it("supports bracket path", () => {
// const obj = { x: "y" };
// dot.setter(obj, "nested[0]", "value");
// expect(obj).toEqual({ x: "y", nested: ["value"] });
// });

it("supports path containing key of the object", () => {
const obj = { x: "y" };
dot.setter(obj, "a.x.c", "value");
expect(obj).toEqual({ x: "y", a: { x: { c: "value" } } });
});

it("can convert primitives to objects before setting", () => {
const obj = { x: [{ y: true }] };
dot.setter(obj, "x.0.y.z", true);
expect(obj).toEqual({ x: [{ y: { z: true } }] });
});
});
80 changes: 80 additions & 0 deletions src/lib/dot/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

export type DotPath = string | string[];

/**
* Get value with given path
*/

const getter = (obj: any, path: DotPath, defaultValue?: any): any => {
path = toPath(path);
let index = 0;
while (obj && index < path.length) {
obj = obj[path[index++]];
}
return obj === undefined ? defaultValue : obj;
};

/**
* Set value with given path
*/

const setter = (obj: any, path: DotPath, value: any) => {
if (!isObject) return obj;
path = toPath(path);

let index = -1;
const length = path.length;
const lastIndex = length - 1;
let nested = obj;

while (nested != null && ++index < length) {
const key = path[index];
let newValue = value;

if (index !== lastIndex) {
const objValue = nested[key];
newValue = isObject(objValue)
? objValue
: isInteger(path[index + 1])
? []
: {};
}

if (newValue === undefined) {
delete nested[key];
} else {
nested[key] = newValue;
}

nested = nested[key];
}
};

export default {
getter,
setter,
};

const toPath = (path: DotPath): string[] => {
if (Array.isArray(path)) return path;
return path.split(".");
};

/**
* Checks if `value` is the object
* (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
*/

const isObject = (value: any) => {
const type = typeof value;
return value !== null && (type === "object" || type === "function");
};

/**
* Checks if `value` is the integer
*/

const isInteger = (value: any): boolean => {
return String(Math.floor(Number(value))) === value;
};

0 comments on commit e31e172

Please sign in to comment.