@finnair/path
Advanced tools
Comparing version 5.4.0 to 6.0.0
@@ -6,2 +6,12 @@ # Change Log | ||
# [6.0.0](https://github.com/finnair/v-validation/compare/v5.4.0...v6.0.0) (2024-09-16) | ||
### Features | ||
- @finnair/diff package with Diff and VersionInfo ([#111](https://github.com/finnair/v-validation/issues/111)) ([3b26d49](https://github.com/finnair/v-validation/commit/3b26d49b63851fbcfce9b15efc53ad5418ae4de4)) | ||
### BREAKING CHANGES | ||
- More general and efficient PathMatcher API. | ||
# [5.4.0](https://github.com/finnair/v-validation/compare/v5.3.0...v5.4.0) (2024-05-08) | ||
@@ -8,0 +18,0 @@ |
@@ -13,6 +13,5 @@ import { Path, PathComponent } from './Path.js'; | ||
export declare function isPathExpression(component: PathComponent | PathExpression): component is PathExpression; | ||
export declare class Node { | ||
export interface Node { | ||
readonly path: Path; | ||
readonly value: any; | ||
constructor(path: Path, value: any); | ||
} | ||
@@ -19,0 +18,0 @@ export declare class IndexMatcher implements PathExpression { |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.AnyProperty = exports.AnyIndex = exports.UnionMatcher = exports.PropertyMatcher = exports.IndexMatcher = exports.Node = exports.isPathExpression = void 0; | ||
exports.AnyProperty = exports.AnyIndex = exports.UnionMatcher = exports.PropertyMatcher = exports.IndexMatcher = exports.isPathExpression = void 0; | ||
const Path_js_1 = require("./Path.js"); | ||
@@ -9,11 +9,2 @@ function isPathExpression(component) { | ||
exports.isPathExpression = isPathExpression; | ||
class Node { | ||
path; | ||
value; | ||
constructor(path, value) { | ||
this.path = path; | ||
this.value = value; | ||
} | ||
} | ||
exports.Node = Node; | ||
class IndexMatcher { | ||
@@ -20,0 +11,0 @@ index; |
import { Path, PathComponent } from './Path.js'; | ||
import { Node, PathExpression, PropertyMatcher, IndexMatcher, AnyIndex, AnyProperty, UnionMatcher } from './matchers.js'; | ||
export interface ResultCollector { | ||
/** | ||
* Collect a matching path and value. Return true to continue matching or false to stop. | ||
*/ | ||
(path: Path, value: any): boolean; | ||
} | ||
export declare class PathMatcher { | ||
@@ -7,8 +13,27 @@ private readonly expressions; | ||
private constructor(); | ||
find(root: any, first?: boolean): Node[]; | ||
findFirst(root: any): undefined | Node; | ||
findValues(root: any, first?: boolean): any[]; | ||
findFirstValue(root: any): any; | ||
find(root: any, collector: ResultCollector): void; | ||
findAll(root: any, acceptUndefined?: boolean): Node[]; | ||
findFirst(root: any, acceptUndefined?: boolean): undefined | Node; | ||
findValues(root: any, acceptUndefined?: boolean): any[]; | ||
findFirstValue(root: any, acceptUndefined?: boolean): any; | ||
/** | ||
* Exact match: path length must match the number of expressions and all expressions must match. Only sibling paths match. | ||
* | ||
* @param path | ||
* @returns true if path is an exact match to expressions | ||
*/ | ||
match(path: Path): boolean; | ||
/** | ||
* Prefix match: path length must be equal or longer than the number of expressions and all expressions must match. All sibling and child paths match. | ||
* | ||
* @param path | ||
* @returns true the start the path matches | ||
*/ | ||
prefixMatch(path: Path): boolean; | ||
/** | ||
* Partial match: path length can be less than or more than the number of expressions, but all corresponding expressions must match. All parent, sibling and child paths match. | ||
* | ||
* @param path | ||
* @returns true if all path components match | ||
*/ | ||
partialMatch(path: Path): boolean; | ||
@@ -15,0 +40,0 @@ toJSON(): string; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.UnionMatcher = exports.Node = exports.IndexMatcher = exports.PropertyMatcher = exports.AnyProperty = exports.AnyIndex = exports.PathMatcher = void 0; | ||
exports.UnionMatcher = exports.IndexMatcher = exports.PropertyMatcher = exports.AnyProperty = exports.AnyIndex = exports.PathMatcher = void 0; | ||
const Path_js_1 = require("./Path.js"); | ||
const matchers_js_1 = require("./matchers.js"); | ||
Object.defineProperty(exports, "Node", { enumerable: true, get: function () { return matchers_js_1.Node; } }); | ||
Object.defineProperty(exports, "PropertyMatcher", { enumerable: true, get: function () { return matchers_js_1.PropertyMatcher; } }); | ||
@@ -23,11 +22,10 @@ Object.defineProperty(exports, "IndexMatcher", { enumerable: true, get: function () { return matchers_js_1.IndexMatcher; } }); | ||
} | ||
find(root, first) { | ||
find(root, collector) { | ||
if (this.expressions.length === 0) { | ||
return [new matchers_js_1.Node(Path_js_1.Path.ROOT, root)]; | ||
collector(Path_js_1.Path.ROOT, root); | ||
} | ||
if (typeof root !== 'object') { | ||
return []; | ||
return; | ||
} | ||
const currentPath = []; | ||
const results = []; | ||
const handlers = []; | ||
@@ -39,3 +37,2 @@ for (let i = 0; i < this.expressions.length - 1; i++) { | ||
this.expressions[0].find(root, handlers[0]); | ||
return results; | ||
function intermediateHandler(index, expressions) { | ||
@@ -49,40 +46,56 @@ return (value, component) => { | ||
return (value, component) => { | ||
results.push(new matchers_js_1.Node(Path_js_1.Path.of(...currentPath, component), value)); | ||
return !first; | ||
return collector(Path_js_1.Path.of(...currentPath, component), value); | ||
}; | ||
} | ||
} | ||
findFirst(root) { | ||
return this.find(root, true)[0]; | ||
findAll(root, acceptUndefined) { | ||
const results = []; | ||
this.find(root, (path, value) => { | ||
if (value !== undefined || acceptUndefined) { | ||
results.push({ path, value }); | ||
return true; | ||
} | ||
return true; | ||
}); | ||
return results; | ||
} | ||
findValues(root, first) { | ||
if (this.expressions.length === 0) { | ||
return [root]; | ||
} | ||
if (typeof root !== 'object') { | ||
return []; | ||
} | ||
findFirst(root, acceptUndefined) { | ||
let result = undefined; | ||
this.find(root, (path, value) => { | ||
if (value !== undefined || acceptUndefined) { | ||
result = { path, value }; | ||
return false; | ||
} | ||
return true; | ||
}); | ||
return result; | ||
} | ||
findValues(root, acceptUndefined) { | ||
const results = []; | ||
const handlers = []; | ||
for (let i = 0; i < this.expressions.length - 1; i++) { | ||
handlers[i] = intermediateHandler(i, this.expressions); | ||
} | ||
handlers[this.expressions.length - 1] = resultHandler(); | ||
this.expressions[0].find(root, handlers[0]); | ||
this.find(root, (path, value) => { | ||
if (value !== undefined || acceptUndefined) { | ||
results.push(value); | ||
return true; | ||
} | ||
return true; | ||
}); | ||
return results; | ||
function intermediateHandler(index, expressions) { | ||
return (value) => { | ||
return expressions[index + 1].find(value, handlers[index + 1]); | ||
}; | ||
} | ||
function resultHandler() { | ||
return (value) => { | ||
results.push(value); | ||
return !first; | ||
}; | ||
} | ||
} | ||
findFirstValue(root) { | ||
return this.findValues(root, true)[0]; | ||
findFirstValue(root, acceptUndefined) { | ||
let result = undefined; | ||
this.find(root, (path, value) => { | ||
if (value !== undefined || acceptUndefined) { | ||
result = value; | ||
return false; | ||
} | ||
return true; | ||
}); | ||
return result; | ||
} | ||
/** | ||
* Exact match: path length must match the number of expressions and all expressions must match. Only sibling paths match. | ||
* | ||
* @param path | ||
* @returns true if path is an exact match to expressions | ||
*/ | ||
match(path) { | ||
@@ -92,4 +105,3 @@ if (path.length !== this.expressions.length) { | ||
} | ||
let index = 0; | ||
for (; index < this.expressions.length; index++) { | ||
for (let index = 0; index < this.expressions.length; index++) { | ||
if (!this.expressions[index].test(path.componentAt(index))) { | ||
@@ -101,2 +113,8 @@ return false; | ||
} | ||
/** | ||
* Prefix match: path length must be equal or longer than the number of expressions and all expressions must match. All sibling and child paths match. | ||
* | ||
* @param path | ||
* @returns true the start the path matches | ||
*/ | ||
prefixMatch(path) { | ||
@@ -106,4 +124,3 @@ if (path.length < this.expressions.length) { | ||
} | ||
let index = 0; | ||
for (; index < this.expressions.length; index++) { | ||
for (let index = 0; index < this.expressions.length; index++) { | ||
if (!this.expressions[index].test(path.componentAt(index))) { | ||
@@ -115,5 +132,10 @@ return false; | ||
} | ||
/** | ||
* Partial match: path length can be less than or more than the number of expressions, but all corresponding expressions must match. All parent, sibling and child paths match. | ||
* | ||
* @param path | ||
* @returns true if all path components match | ||
*/ | ||
partialMatch(path) { | ||
let index = 0; | ||
for (; index < this.expressions.length && index < path.length; index++) { | ||
for (let index = 0; index < this.expressions.length && index < path.length; index++) { | ||
if (!this.expressions[index].test(path.componentAt(index))) { | ||
@@ -120,0 +142,0 @@ return false; |
@@ -70,6 +70,6 @@ "use strict"; | ||
function include(input, matcher, output) { | ||
matcher.find(input).forEach(node => node.path.set(output, node.value)); | ||
matcher.find(input, (path, value) => path.set(output, value)); | ||
} | ||
function exclude(output, matcher) { | ||
matcher.find(output).forEach(node => node.path.unset(output)); | ||
matcher.find(output, (path) => path.unset(output)); | ||
} | ||
@@ -76,0 +76,0 @@ function jsonClone(value) { |
@@ -13,6 +13,5 @@ import { Path, PathComponent } from './Path.js'; | ||
export declare function isPathExpression(component: PathComponent | PathExpression): component is PathExpression; | ||
export declare class Node { | ||
export interface Node { | ||
readonly path: Path; | ||
readonly value: any; | ||
constructor(path: Path, value: any); | ||
} | ||
@@ -19,0 +18,0 @@ export declare class IndexMatcher implements PathExpression { |
@@ -5,10 +5,2 @@ import { Path } from './Path.js'; | ||
} | ||
export class Node { | ||
path; | ||
value; | ||
constructor(path, value) { | ||
this.path = path; | ||
this.value = value; | ||
} | ||
} | ||
export class IndexMatcher { | ||
@@ -15,0 +7,0 @@ index; |
import { Path, PathComponent } from './Path.js'; | ||
import { Node, PathExpression, PropertyMatcher, IndexMatcher, AnyIndex, AnyProperty, UnionMatcher } from './matchers.js'; | ||
export interface ResultCollector { | ||
/** | ||
* Collect a matching path and value. Return true to continue matching or false to stop. | ||
*/ | ||
(path: Path, value: any): boolean; | ||
} | ||
export declare class PathMatcher { | ||
@@ -7,8 +13,27 @@ private readonly expressions; | ||
private constructor(); | ||
find(root: any, first?: boolean): Node[]; | ||
findFirst(root: any): undefined | Node; | ||
findValues(root: any, first?: boolean): any[]; | ||
findFirstValue(root: any): any; | ||
find(root: any, collector: ResultCollector): void; | ||
findAll(root: any, acceptUndefined?: boolean): Node[]; | ||
findFirst(root: any, acceptUndefined?: boolean): undefined | Node; | ||
findValues(root: any, acceptUndefined?: boolean): any[]; | ||
findFirstValue(root: any, acceptUndefined?: boolean): any; | ||
/** | ||
* Exact match: path length must match the number of expressions and all expressions must match. Only sibling paths match. | ||
* | ||
* @param path | ||
* @returns true if path is an exact match to expressions | ||
*/ | ||
match(path: Path): boolean; | ||
/** | ||
* Prefix match: path length must be equal or longer than the number of expressions and all expressions must match. All sibling and child paths match. | ||
* | ||
* @param path | ||
* @returns true the start the path matches | ||
*/ | ||
prefixMatch(path: Path): boolean; | ||
/** | ||
* Partial match: path length can be less than or more than the number of expressions, but all corresponding expressions must match. All parent, sibling and child paths match. | ||
* | ||
* @param path | ||
* @returns true if all path components match | ||
*/ | ||
partialMatch(path: Path): boolean; | ||
@@ -15,0 +40,0 @@ toJSON(): string; |
import { Path } from './Path.js'; | ||
import { Node, PropertyMatcher, IndexMatcher, AnyIndex, AnyProperty, UnionMatcher, isPathExpression } from './matchers.js'; | ||
import { PropertyMatcher, IndexMatcher, AnyIndex, AnyProperty, UnionMatcher, isPathExpression } from './matchers.js'; | ||
export class PathMatcher { | ||
@@ -14,11 +14,10 @@ expressions; | ||
} | ||
find(root, first) { | ||
find(root, collector) { | ||
if (this.expressions.length === 0) { | ||
return [new Node(Path.ROOT, root)]; | ||
collector(Path.ROOT, root); | ||
} | ||
if (typeof root !== 'object') { | ||
return []; | ||
return; | ||
} | ||
const currentPath = []; | ||
const results = []; | ||
const handlers = []; | ||
@@ -30,3 +29,2 @@ for (let i = 0; i < this.expressions.length - 1; i++) { | ||
this.expressions[0].find(root, handlers[0]); | ||
return results; | ||
function intermediateHandler(index, expressions) { | ||
@@ -40,40 +38,56 @@ return (value, component) => { | ||
return (value, component) => { | ||
results.push(new Node(Path.of(...currentPath, component), value)); | ||
return !first; | ||
return collector(Path.of(...currentPath, component), value); | ||
}; | ||
} | ||
} | ||
findFirst(root) { | ||
return this.find(root, true)[0]; | ||
findAll(root, acceptUndefined) { | ||
const results = []; | ||
this.find(root, (path, value) => { | ||
if (value !== undefined || acceptUndefined) { | ||
results.push({ path, value }); | ||
return true; | ||
} | ||
return true; | ||
}); | ||
return results; | ||
} | ||
findValues(root, first) { | ||
if (this.expressions.length === 0) { | ||
return [root]; | ||
} | ||
if (typeof root !== 'object') { | ||
return []; | ||
} | ||
findFirst(root, acceptUndefined) { | ||
let result = undefined; | ||
this.find(root, (path, value) => { | ||
if (value !== undefined || acceptUndefined) { | ||
result = { path, value }; | ||
return false; | ||
} | ||
return true; | ||
}); | ||
return result; | ||
} | ||
findValues(root, acceptUndefined) { | ||
const results = []; | ||
const handlers = []; | ||
for (let i = 0; i < this.expressions.length - 1; i++) { | ||
handlers[i] = intermediateHandler(i, this.expressions); | ||
} | ||
handlers[this.expressions.length - 1] = resultHandler(); | ||
this.expressions[0].find(root, handlers[0]); | ||
this.find(root, (path, value) => { | ||
if (value !== undefined || acceptUndefined) { | ||
results.push(value); | ||
return true; | ||
} | ||
return true; | ||
}); | ||
return results; | ||
function intermediateHandler(index, expressions) { | ||
return (value) => { | ||
return expressions[index + 1].find(value, handlers[index + 1]); | ||
}; | ||
} | ||
function resultHandler() { | ||
return (value) => { | ||
results.push(value); | ||
return !first; | ||
}; | ||
} | ||
} | ||
findFirstValue(root) { | ||
return this.findValues(root, true)[0]; | ||
findFirstValue(root, acceptUndefined) { | ||
let result = undefined; | ||
this.find(root, (path, value) => { | ||
if (value !== undefined || acceptUndefined) { | ||
result = value; | ||
return false; | ||
} | ||
return true; | ||
}); | ||
return result; | ||
} | ||
/** | ||
* Exact match: path length must match the number of expressions and all expressions must match. Only sibling paths match. | ||
* | ||
* @param path | ||
* @returns true if path is an exact match to expressions | ||
*/ | ||
match(path) { | ||
@@ -83,4 +97,3 @@ if (path.length !== this.expressions.length) { | ||
} | ||
let index = 0; | ||
for (; index < this.expressions.length; index++) { | ||
for (let index = 0; index < this.expressions.length; index++) { | ||
if (!this.expressions[index].test(path.componentAt(index))) { | ||
@@ -92,2 +105,8 @@ return false; | ||
} | ||
/** | ||
* Prefix match: path length must be equal or longer than the number of expressions and all expressions must match. All sibling and child paths match. | ||
* | ||
* @param path | ||
* @returns true the start the path matches | ||
*/ | ||
prefixMatch(path) { | ||
@@ -97,4 +116,3 @@ if (path.length < this.expressions.length) { | ||
} | ||
let index = 0; | ||
for (; index < this.expressions.length; index++) { | ||
for (let index = 0; index < this.expressions.length; index++) { | ||
if (!this.expressions[index].test(path.componentAt(index))) { | ||
@@ -106,5 +124,10 @@ return false; | ||
} | ||
/** | ||
* Partial match: path length can be less than or more than the number of expressions, but all corresponding expressions must match. All parent, sibling and child paths match. | ||
* | ||
* @param path | ||
* @returns true if all path components match | ||
*/ | ||
partialMatch(path) { | ||
let index = 0; | ||
for (; index < this.expressions.length && index < path.length; index++) { | ||
for (let index = 0; index < this.expressions.length && index < path.length; index++) { | ||
if (!this.expressions[index].test(path.componentAt(index))) { | ||
@@ -135,2 +158,2 @@ return false; | ||
} | ||
export { AnyIndex, AnyProperty, PropertyMatcher, IndexMatcher, Node, UnionMatcher }; | ||
export { AnyIndex, AnyProperty, PropertyMatcher, IndexMatcher, UnionMatcher }; |
@@ -65,6 +65,6 @@ import { PathMatcher } from './PathMatcher.js'; | ||
function include(input, matcher, output) { | ||
matcher.find(input).forEach(node => node.path.set(output, node.value)); | ||
matcher.find(input, (path, value) => path.set(output, value)); | ||
} | ||
function exclude(output, matcher) { | ||
matcher.find(output).forEach(node => node.path.unset(output)); | ||
matcher.find(output, (path) => path.unset(output)); | ||
} | ||
@@ -71,0 +71,0 @@ function jsonClone(value) { |
{ | ||
"name": "@finnair/path", | ||
"version": "5.4.0", | ||
"version": "6.0.0", | ||
"private": false, | ||
@@ -40,3 +40,3 @@ "description": "Simple object path as array of strings and numbers", | ||
}, | ||
"gitHead": "8c3d3213f21cef7c27ed0d70bf8596d484a3d9b1" | ||
"gitHead": "c1075c7dc35e540949abc4e220547e1146523682" | ||
} |
@@ -13,3 +13,3 @@ # Path | ||
Install v-validation using [`yarn`](https://yarnpkg.com/en/package/jest): | ||
Install v-validation using [`yarn`](https://yarnpkg.com): | ||
@@ -28,45 +28,10 @@ ```bash | ||
### Report Validation Errors | ||
### Validation | ||
Path can be used to point a location of invalid value (see [`v-validation`](../core/README.md). Path's immutability and fluent API makes it easy and safe to use. | ||
### Analyze Changes | ||
### Diff, Versioning & Patching | ||
Path can be used to report changes made to an object: | ||
Analyze changes and trigger logic based on what has changed (see [`diff`](../diff/README.md). | ||
```typescript | ||
// NOTE: This is pseudocode! | ||
const originalValue = fetchResource(); // No need for long lasting, e.g. optimistic, locking | ||
const updatedValue = await doUpdate(originalValue); // Edit in e.g. UI | ||
// Map from Path to new value | ||
const changes: Map<Path, any> = analyzeChanges(updatedValue, originalValue); | ||
// analyzeChanges implementation is not in this library's scope | ||
``` | ||
### Detect Interesting Changes | ||
In message based systems, consumers may be interested only in specific changes. Once a change is analyzed, `PathMatcher` can be used to check if it's of interest to a particular consumer. | ||
```typescript | ||
const subscription = [PathMatcher.of('interestingProperty'), PathMatcher.of('interestingArray', AnyIndex, 'someProperty')]; | ||
const isInteresting = Array.from(changes.keys()).some(path => subscription.some(pathMatcher => pathMatcher.prefixMatch(path))); | ||
``` | ||
### Apply Changes (Patch) | ||
Changes can also be applied to another (newer) version of the same object safely. This is important in systems where there | ||
```typescript | ||
// Fetch and lock (pessimistic or optimistic) latest value for the duration of the actual update | ||
const latestValue = fetchAndLockResource(); | ||
changes.forEach((newValue, path) => { | ||
path.set(latestValue, newValue); // Unsets value if newValue is undefined | ||
}); | ||
updateResource(latestValue); | ||
``` | ||
NOTE: Updating an array with `Path.set` will truncate possible undefined values from the end of the array. This allows | ||
removing trailing elements without leaving undefined elements in place. | ||
### Include/Exclude Projection | ||
@@ -73,0 +38,0 @@ |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Unpublished package
Supply chain riskPackage version was not found on the registry. It may exist on a different registry and need to be configured to pull from that registry.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
93736
1353
272
1