object-scan

repository·master·Indexed 41 days ago

https://github.com/blackflux/object-scan

A high-performance, dependency-free utility for traversing complex object hierarchies using matching patterns and callbacks. It supports wildcards, regex, Or Clauses, and arbitrary depth matching to efficiently locate elements within an object structure.

Tokens
11.4K
Snippets
34
Records
57
Agent score
50%

What's inside object-scan

  1. Overview of object-scan features

    master

    Object-scan is a high-performance, dependency-free utility for traversing object hierarchies using matching patterns and callbacks. Key features include:

    • Efficient Traversal: Each input is traversed at most once during a search.
    • Powerful Matching: Supports complex syntax including wildcards, regex, and logical OR clauses.
    • Small Footprint: Tiny bundle size and no external dependencies.
    • High Performance: Optimized for speed compared to competitors.
  2. Use the @blackflux/robo-config-plugin for npm package configuration

    master

    The @blackflux/robo-config-plugin is a plugin designed to manage various configuration files for open source npm packages hosted on GitHub. It provides standardized templates and configurations for a wide range of development tools and platforms, including:

    • Project Structure: Templates for base npm structures, testing (Mocha, index tests), and documentation (README).
    • Code Quality: ESLint configurations and editor settings (e.g., two-space indentation).
    • CI/CD & Automation: Configurations for CircleCI, Dependabot, and Semantic Release (including commit conventions).
    • Package Management: Standardized .npmignore, package.json merging strategies (shallow vs. deep), and license files (MIT).
    • Developer Tools: JetBrains IDE settings and Git configuration (.gitignore).
    • Metadata & Badges: NPM status, download counts, and CircleCI badges.
    • Coverage: C8 coverage configurations with various merging strategies.
    Manage various configuration files for an open source npm package on github.
  3. Exclude paths using the exclamation mark

    master

    Use the ! character to exclude specific paths from your match results.

    Examples:

    • {a,b},!a: Matches a or b, but excludes a (effectively only matching b).
    • **,!**.a: Matches everything except paths that end in .a.
    • [*], [!(^[01]$)]: Matches all array elements except those that are not 0 or 1 (using regex exclusion).
    // Only 'b'
    const haystack1 = { a: 0, b: 1 };
    objectScan(['{a,b},!a'], { joined: true, strict: false })(haystack1);
    // => [ 'b' ]
    
    // All except ending in 'a'
    const haystack2 = { a: 0, b: { a: 1, c: 2 } };
    objectScan(['**,!**.a'], { joined: true })(haystack2);
    // => [ 'b.c', 'b' ]
  4. Use Regex for advanced pattern matching

    master

    Regex patterns can be used within selectors by wrapping the expression in parentheses (...). This can be applied to both array indices and object keys.

    Examples:

    • (^foo): Matches keys starting with foo.
    • [(5)]: Matches array indices containing the digit 5.
    • [(^[01]$)]: Matches array indices that are exactly 0 or 1.
    // Starting with 'foo'
    const haystack1 = { foo: 0, foobar: 1, bar: 2 };
    objectScan(['(^foo)'], { joined: true })(haystack1);
    // => [ 'foobar', 'foo' ]
    
    // Containing '5'
    const haystack2 = [...Array(20).keys()];
    objectScan(['[(5)]'], { joined: true })(haystack2);
    // => [ '[15]', '[5]' ]
    
    // Exactly [0] or [1]
    const haystack3 = ['a', 'b', 'c', 'd'];
    objectScan(['[(^[01]$)]'], { joined: true })(haystack3);
    // => [ '[1]', '[0]' ]
  5. Match at arbitrary depth using ** and ++

    master

    You can match elements at varying depths using the following operators:

    • **: Matches zero or more nestings.
    • ++: Matches one or more nestings.

    These can be combined with Regex or Or Clauses by prepending them. For example, **(1) matches any level that contains the value 1.

    // Zero or more nestings under 'a'
    const haystack1 = { a: { b: 0, c: 0 } };
    objectScan(['a.**'], { joined: true })(haystack1);
    // => [ 'a.c', 'a.b', 'a' ]
    
    // One or more nestings under 'a'
    const haystack2 = { a: { b: 0, c: 0 } };
    objectScan(['a.++'], { joined: true })(haystack2);
    // => [ 'a.c', 'a.b' ]
    
    // All levels containing '1'
    const haystack3 = { 1: { 1: ['c', 'd'] }, 510: 'e', foo: { 1: 'f' } };
    objectScan(['**(1)'], { joined: true })(haystack3);
    // => [ '510', '1.1[1]', '1.1', '1' ]
  6. Use wildcards for pattern matching

    master

    Wildcards allow for flexible pattern matching in both object keys and array indices. The following special characters are supported:

    • *: Match zero or more characters
    • +: Match one or more characters
    • ?: Match exactly one character
    • \: Escape the subsequent character

    Examples:

    • foo*: Matches keys starting with foo.
    • *: Matches all top-level elements.
    • [?5]: Matches array indices that are two digits and end in 5 (e.g., [15], [25]).
    • a.+.c: Matches nested paths where the middle segment has one or more characters (e.g., a.b.c).
    // Starting with 'foo'
    const haystack1 = { foo: 0, foobar: 1, bar: 2 };
    objectScan(['foo*'], { joined: true })(haystack1);
    // => [ 'foobar', 'foo' ]
    
    // Top level
    const haystack2 = { a: { b: 0, c: 1 }, d: 2 };
    objectScan(['*'], { joined: true })(haystack2);
    // => [ 'd', 'a' ]
    
    // Two digit ending in five
    const haystack3 = [...Array(30).keys()];
    objectScan(['[?5]'], { joined: true })(haystack3);
    // => [ '[25]', '[15]' ]
    
    // Nested with '+'
    const haystack4 = { a: { b: { c: 0 }, d: { f: 0 } } };
    objectScan(['a.+.c'], { joined: true })(haystack4);
    // => [ 'a.b.c' ]
  7. Perform nested path recursion with **{...} or ++{...}

    master

    To match a specific path pattern recursively through a structure, combine arbitrary depth operators with an Or Clause.

    • **{...}: Matches the path(s) in the Or Clause zero or more times.
    • ++{...}: Matches the path(s) in the Or Clause one or more times.

    Examples:

    • **{[*]} : Traverses only through arrays.
    • **{*} : Traverses only through objects.
    • a.**{b.c}: Matches b.c zero or more times under a.
    // Cyclic path
    const haystack1 = [[[[0, 1], [1, 2]], [[3, 4], [5, 6]]], [[[7, 8], [9, 10]], [[11, 12], [13, 14]]]];
    objectScan(['++{[0][1]}'], { joined: true })(haystack1);
    // => [ '[0][1][0][1]', '[0][1]' ]
    
    // Traverse only arrays
    const haystack2 = [[[{ a: [1] }], [2]]];
    objectScan(['**{[*]}'], { joined: true })(haystack2);
    // => [ '[0][1][0]', '[0][1]', '[0][0][0]', '[0][0]', '[0]' ]
    
    // Zero or more times
    const haystack3 = { a: { b: { c: { b: { c: 0 } } } } };
    objectScan(['a.**{b.c}'], { joined: true })(haystack3);
    // => [ 'a.b.c.b.c', 'a.b.c', 'a' ]
  8. Escape special characters in keys

    master

    If your object keys contain special characters used by the matching syntax, you must escape them with a backslash \.

    Special characters requiring escaping: [, ], {, }, (, ), ,, ., !, ?, *, +, and \.

    // Matching a special object key '[1]'
    const haystack = { '[1]': 0 };
    objectScan(['\[1\]'], { joined: true })(haystack);
    // => [ '\[1\]' ]
  9. Understand object-scan traversal behavior

    master

    Understanding how object-scan interacts with data structures is critical for predictable results:

    • Depth-First: Traversal is always depth-first, which helps maintain lower memory consumption.
    • Array String Keys: String keys attached to an Array object (e.g., arr.str = 'val') are not traversed.
    • Sparse Arrays: Only set keys (indices that actually contain values) are traversed; empty entries in sparse arrays are skipped.
    • Recursion-Free: The implementation is completely recursion-free, meaning it can safely traverse extremely deeply nested objects without triggering a Maximum call stack size exceeded error.
    • Performance: The library uses a two-stage process: 1. Initialization (parsing needles into a search tree) and 2. Search (traversing the input simultaneously with the search tree). This makes applying the same search to multiple different inputs very efficient.
  10. Use search context to manage state

    master

    A context object can be passed as the second argument to a search invocation. It is available in all callbacks via the context property. This allows you to manage state across a search invocation without recompiling the search.

    By default, matched keys are returned. However, if a context is provided, the context itself is returned as the final result (unless rtn is specified otherwise).

    const haystack = { a: { b: { c: 2, d: 11 }, e: 7 } };
    objectScan(['**.{c,d,e}'], {
      joined: true,
      filterFn: ({ value, context }) => { context.sum += value; }
    })(haystack, { sum: 0 });
    // => { sum: 20 }
  11. Match object properties using dot notation

    master

    To match an object property, use the property name directly in the needle expression. This does not match array indices.

    const haystack = { foo: 0, bar: 1 };
    objectScan(['foo'], { joined: true })(haystack);
    // => [ 'foo' ]
  12. Use Array Needles for faster, exact path matching

    master

    Instead of strings, you can pass needles as arrays of integers (for array indices) and strings (for object keys).

    Benefits:

    • Matches array keys with integers and object keys with strings.
    • Does not support other matching syntax (no wildcards/regex).
    • Does not require escaping.
    • Parses faster than string needles.
    • Allows the key result from objectScan to be passed back into itself.

    Note: matchedBy and similar options contain the original needles, not copies. Use useArraySelector: false if you want to avoid the [n] notation in the output.

    // Mixed path: 'a', then index 0, then 'b'
    const haystack1 = { a: [{ b: 0 }] };
    objectScan([['a', 0, 'b']], { joined: true })(haystack1);
    // => [ 'a[0].b' ]
    
    // Using array needles with 'rtn: value'
    const haystack2 = { 'a.b': [0], a: { b: [1] } };
    objectScan([['a.b', 0]], { joined: true, rtn: 'value' })(haystack2);
    // => [ 0 ]
    
    // Mixed needles (array needles + string wildcard needle)
    const haystack3 = { a: [{ b: 0 }, { b: 0 }] };
    objectScan([['a', 0, 'b'], ['a', 1, 'b'], 'a[*].b'], { joined: true, rtn: 'matchedBy' })(haystack3);
    // => [ [ [ 'a', 1, 'b' ], 'a[*].b' ], [ [ 'a', 0, 'b' ], 'a[*].b' ] ]