Skip to content

Commit

Permalink
feat(path): re-implemented path handling
Browse files Browse the repository at this point in the history
Supports dot-style and array style (as before, although dot-style much improved to work with Ajv 6)
and also adds JSON Pointer-style path support.

BREAKING CHANGE: Path handling incompatible; requires leading ., and dataPath is replaced with path,
dotPath, pointerPath
  • Loading branch information
grantila committed Feb 9, 2022
1 parent f9396c2 commit f0a47f8
Show file tree
Hide file tree
Showing 9 changed files with 367 additions and 59 deletions.
1 change: 1 addition & 0 deletions .github/workflows/branches.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ jobs:
- 12.x
- 14.x
- 16.x
- 17.x
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/master.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
- 12.x
- 14.x
- 16.x
- 17.x
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
Expand Down
36 changes: 30 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Given the following JSON:
}
```

The position of `foo.bar` (or `["foo", "bar"]` if provided as an array), is:
The position of `/foo/bar` (or `["foo", "bar"]` if provided as an array), is:
```js
{
start: { line: 3, column: 16, offset: 30 },
Expand All @@ -39,7 +39,12 @@ If the *property* "bar" is wanted, instead of the *value*, set `markIdentifier`
## Versions

* Since v2 this is a [pure ESM][pure-esm] package, and requires Node.js >=12.20

* Since v3 the API has changed. The `dataPath` option has been renamed with changed semantics.
* Dot-based (string) `dataPath` is now `dotPath`. **It's not recommended to use as it's not safe for certain characters**.
* Also, it now requires an initial `.`. Only the path `.` represents the root object.
* Array-based `dataPath` is now simply `path`.
* An empty object represents the root object, like in v2.
* New slash-based (string) `pointerPath` is allowed, following JSON Pointer encoding.

# Simple usage

Expand All @@ -51,8 +56,12 @@ where `LocationOptions` is:

```ts
interface LocationOptions
dataPath: string | Array< string | number >;
markIdentifier?: boolean;

// Only one of the following
dotPath: string;
path: Array< string | number >;
pointerPath: string;
}
```

Expand All @@ -76,25 +85,40 @@ interface Position
}
```

### As textual path:
### As dot-separated textual path:

```ts
import { jsonpos } from 'jsonpos'

const loc = jsonpos(
'{ "foo": { "bar": "baz" } }',
{ dotPath: 'foo.bar' }
);
```

*Note that this method is strongly advised against.*


### As /-separated textual path:

```ts
import { jsonpos } from 'jsonpos'

const loc = jsonpos(
'{ "foo": { "bar": "baz" } }',
{ dataPath: 'foo.bar' }
{ pointerPath: 'foo/bar' }
);
```


### As array path:

```ts
import { jsonpos } from 'jsonpos'

const loc = jsonpos(
'{ "foo": { "bar": "baz" } }',
{ dataPath: [ 'foo', 'bar' ] }
{ path: [ 'foo', 'bar' ] }
);
```

Expand Down
6 changes: 3 additions & 3 deletions lib/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as index from "./index"
import * as index from "./index.js"


const { jsonpos } = index;
Expand All @@ -7,7 +7,7 @@ describe( "index", ( ) =>
{
it( "jsonpos by string", ( ) =>
{
const res = jsonpos( '{"foo": "bar"}', { dataPath: '.foo' } );
const res = jsonpos( '{"foo": "bar"}', { dotPath: '.foo' } );
expect( res ).toStrictEqual( {
start: { line: 1, column: 9, offset: 8 },
end: { line: 1, column: 14, offset: 13 },
Expand All @@ -16,7 +16,7 @@ describe( "index", ( ) =>

it( "jsonpos by string", ( ) =>
{
const res = jsonpos( { "foo": "bar" } as any, { dataPath: '.foo' } );
const res = jsonpos( { "foo": "bar" } as any, { dotPath: '.foo' } );
expect( res ).toStrictEqual( {
start: { line: 2, column: 12, offset: 13 },
end: { line: 2, column: 17, offset: 18 },
Expand Down
17 changes: 10 additions & 7 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ import type { ParsedJson } from './parse.js'
import { getAstByObject, getAstByString } from './parse.js'

import type {
Position,
LocationOptionsPath,
LocationPath,
} from './path.js'
import { parsePath } from './path.js'

import type {
Position,
Location,
LocationOptions,
} from './location.js'
Expand All @@ -13,12 +18,10 @@ import { getLocation } from './location.js'
export type { ParsedJson }
export { getAstByObject, getAstByString }

export type {
Position,
LocationPath,
Location,
LocationOptions,
}
export type { LocationOptionsPath, LocationPath }
export { parsePath }

export type { Position, Location, LocationOptions }
export { getLocation }


Expand Down
92 changes: 67 additions & 25 deletions lib/location.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getAstByString } from './parse'
import { getLocation } from './location'
import { getAstByString } from './parse.js'
import { getLocation } from './location.js'
import { getAstByObject } from '.';

const json = `{
"foo": [
Expand All @@ -16,7 +17,7 @@ describe( "location", ( ) =>

const loc = getLocation(
parsed,
{ dataPath: "foo.1.baz", markIdentifier: false }
{ dotPath: ".foo.1.baz", markIdentifier: false }
);
expect( loc ).toStrictEqual( {
start: {
Expand All @@ -38,7 +39,7 @@ describe( "location", ( ) =>

const loc = getLocation(
parsed,
{ dataPath: "foo.1.baz" }
{ dotPath: ".foo.1.baz" }
);
expect( loc ).toStrictEqual( {
start: {
Expand All @@ -60,7 +61,7 @@ describe( "location", ( ) =>

const loc = getLocation(
parsed,
{ dataPath: "foo.1.baz", markIdentifier: true }
{ dotPath: ".foo.1.baz", markIdentifier: true }
);
expect( loc ).toStrictEqual( {
start: {
Expand All @@ -76,26 +77,15 @@ describe( "location", ( ) =>
} );
} );

it( "by string beginning with '.', markIdentifier = true", ( ) =>
it( "by string not beginning with '.'", ( ) =>
{
const parsed = getAstByString( json );

const loc = getLocation(
const thrower = ( ) => getLocation(
parsed,
{ dataPath: ".foo.1.baz", markIdentifier: true }
{ dotPath: "foo.1.baz", markIdentifier: true }
);
expect( loc ).toStrictEqual( {
start: {
line: 4,
column: 5,
offset: 25,
},
end: {
line: 4,
column: 10,
offset: 30,
},
} );
expect( thrower ).toThrow( );
} );

it( "by array path, markIdentifier = false", ( ) =>
Expand All @@ -104,7 +94,7 @@ describe( "location", ( ) =>

const loc = getLocation(
parsed,
{ dataPath: [ "foo", 1, "baz" ], markIdentifier: false }
{ path: [ "foo", 1, "baz" ], markIdentifier: false }
);
expect( loc ).toStrictEqual( {
start: {
Expand All @@ -126,7 +116,7 @@ describe( "location", ( ) =>

const loc = getLocation(
parsed,
{ dataPath: [ "foo", 1, "baz" ], markIdentifier: true }
{ path: [ "foo", 1, "baz" ], markIdentifier: true }
);
expect( loc ).toStrictEqual( {
start: {
Expand All @@ -149,7 +139,7 @@ describe( "location", ( ) =>
const thrower = ( ) =>
getLocation(
parsed,
{ dataPath: [ "foo", 1, "bee" ], markIdentifier: false }
{ path: [ "foo", 1, "bee" ], markIdentifier: false }
);
expect( thrower ).toThrow(
/No such property bee in \.foo\.1 .*foo\.1\.bee/
Expand All @@ -163,7 +153,7 @@ describe( "location", ( ) =>
const thrower = ( ) =>
getLocation(
parsed,
{ dataPath: [ "foo", 3, "baz" ], markIdentifier: false }
{ path: [ "foo", 3, "baz" ], markIdentifier: false }
);
expect( thrower ).toThrow(
/Index 3 out-of-bounds .* size 2 at \.foo .*foo\.3\.baz/
Expand All @@ -177,10 +167,62 @@ describe( "location", ( ) =>
const thrower = ( ) =>
getLocation(
parsed,
{ dataPath: [ "foo", "bad", "baz" ], markIdentifier: false }
{ path: [ "foo", "bad", "baz" ], markIdentifier: false }
);
expect( thrower ).toThrow(
/Invalid non-numeric array index "bad" .* \.foo .*foo\.bad\.baz/
);
} );

describe( "Handle difficult characters", ( ) =>
{

it( "dot style", ( ) =>
{
const dotPath = ".foo['baz']['bak']..bam.'bar'['bob'].bee";
const obj =
'{"foo":{"baz":{"bak":{"":{"bam":{"bar":{"bob":"bee"}}}}}}}';

const parsed = getAstByString( obj );

const loc = getLocation( parsed, { dotPath } );

expect( loc ).toStrictEqual( {
start: {
column: 47,
line: 1,
offset: 46,
},
end: {
column: 52,
line: 1,
offset: 51,
},
} );
} );

it( "json-pointer style", ( ) =>
{
const pointerPath = "/foo/a\"b'c~1d~0e[f]g//bar";
const obj = { "foo": { "a\"b'c/d~e[f]g": { "": "bar" } } };

const parsed = getAstByObject( obj );
console.log(parsed.jsonString);

const loc = getLocation( parsed, { pointerPath } );

expect( loc ).toStrictEqual( {
start: {
column: 17,
line: 4,
offset: 59,
},
end: {
column: 22,
line: 4,
offset: 64,
},
} );
} );
} );
} );
27 changes: 9 additions & 18 deletions lib/location.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type { ValueNode, IdentifierNode } from 'json-to-ast'

import type { ParsedJson } from './parse.js'
import { LocationOptionsPath, parsePath } from './path.js'


export type LocationPath = Array< number | string >;

export interface LocationOptions {
dataPath: string | LocationPath;
markIdentifier?: boolean;
}
export type LocationOptions =
& LocationOptionsPath
& {
markIdentifier?: boolean;
};

export interface Position
{
Expand All @@ -25,22 +25,13 @@ export interface Location

export function getLocation(
parsedJson: ParsedJson,
{ dataPath, markIdentifier = false }: LocationOptions
options: LocationOptions
): Location
{
const { jsonAST } = parsedJson;
const { markIdentifier = false } = options;

const path =
Array.isArray( dataPath )
? dataPath
:
(
dataPath.startsWith( '.' )
? dataPath.slice( 1 )
: dataPath
)
.split( '.' )
.filter( val => val );
const path = parsePath( options );

const pathAsString = ( ) => path.join( '.' );
const getParentPath = ( index: number ) =>
Expand Down
Loading

0 comments on commit f0a47f8

Please sign in to comment.