Skip to content

Commit

Permalink
feat: add jsonpath lib (and related helpers) (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
Duologic authored Jun 2, 2023
1 parent 3e494e0 commit e425fcc
Show file tree
Hide file tree
Showing 10 changed files with 711 additions and 1 deletion.
37 changes: 37 additions & 0 deletions array.libsonnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
local d = import 'doc-util/main.libsonnet';

{
'#': d.pkg(
name='array',
url='github.com/jsonnet-libs/xtd/array.libsonnet',
help='`array` implements helper functions for processing arrays.',
),

'#slice':: d.fn(
'`slice` works the same as `std.slice` but with support for negative index/end.',
[
d.arg('indexable', d.T.array),
d.arg('index', d.T.number),
d.arg('end', d.T.number, default='null'),
d.arg('step', d.T.number, default=1),
]
),
slice(indexable, index, end=null, step=1):
local invar = {
index:
if index != null
then
if index < 0
then std.length(indexable) + index
else index
else 0,
end:
if end != null
then
if end < 0
then std.length(indexable) + end
else end
else std.length(indexable),
};
indexable[invar.index:invar.end:step],
}
3 changes: 3 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ in the future, but also provides a place for less general, yet useful utilities.
## Subpackages

* [aggregate](aggregate.md)
* [array](array.md)
* [ascii](ascii.md)
* [camelcase](camelcase.md)
* [date](date.md)
* [inspect](inspect.md)
* [jsonpath](jsonpath.md)
* [string](string.md)
* [url](url.md)
25 changes: 25 additions & 0 deletions docs/array.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
permalink: /array/
---

# package array

```jsonnet
local array = import "github.com/jsonnet-libs/xtd/array.libsonnet"
```

`array` implements helper functions for processing arrays.

## Index

* [`fn slice(indexable, index, end='null', step=1)`](#fn-slice)

## Fields

### fn slice

```ts
slice(indexable, index, end='null', step=1)
```

`slice` works the same as `std.slice` but with support for negative index/end.
53 changes: 53 additions & 0 deletions docs/jsonpath.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
permalink: /jsonpath/
---

# package jsonpath

```jsonnet
local jsonpath = import "github.com/jsonnet-libs/xtd/jsonpath.libsonnet"
```

`jsonpath` implements helper functions to use JSONPath expressions.

## Index

* [`fn convertBracketToDot(path)`](#fn-convertbrackettodot)
* [`fn getJSONPath(source, path)`](#fn-getjsonpath)
* [`fn parseFilterExpr(path)`](#fn-parsefilterexpr)

## Fields

### fn convertBracketToDot

```ts
convertBracketToDot(path)
```

`convertBracketToDot` converts the bracket notation to dot notation.

This function does not support escaping brackets/quotes in path keys.


### fn getJSONPath

```ts
getJSONPath(source, path)
```

`getJSONPath` gets the value at `path` from `source` where path is a JSONPath.

This is a rudimentary implementation supporting the slice operator `[0:3:2]` and
partially supporting filter expressions `?(@.attr==value)`.


### fn parseFilterExpr

```ts
parseFilterExpr(path)
```

`parseFilterExpr` returns a filter function `f(x)` for a filter expression `expr`.

It supports comparisons (<, <=, >, >=) and equality checks (==, !=). If it doesn't
have an operator, it will check if the `expr` value exists.
26 changes: 26 additions & 0 deletions docs/string.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
permalink: /string/
---

# package string

```jsonnet
local string = import "github.com/jsonnet-libs/xtd/string.libsonnet"
```

`string` implements helper functions for processing strings.

## Index

* [`fn splitEscape(str, c, escape='\\')`](#fn-splitescape)

## Fields

### fn splitEscape

```ts
splitEscape(str, c, escape='\\')
```

`split` works the same as `std.split` but with support for escaping the dividing
string `c`.
140 changes: 140 additions & 0 deletions jsonpath.libsonnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
local xtd = import './main.libsonnet';
local d = import 'doc-util/main.libsonnet';

{
'#': d.pkg(
name='jsonpath',
url='github.com/jsonnet-libs/xtd/jsonpath.libsonnet',
help='`jsonpath` implements helper functions to use JSONPath expressions.',
),


'#getJSONPath':: d.fn(
|||
`getJSONPath` gets the value at `path` from `source` where path is a JSONPath.
This is a rudimentary implementation supporting the slice operator `[0:3:2]` and
partially supporting filter expressions `?(@.attr==value)`.
|||,
[
d.arg('source', d.T.any),
d.arg('path', d.T.string,),
d.arg('default', d.T.any, default='null'),
]
),
getJSONPath(source, path, default=null):
local _path = self.convertBracketToDot(path);
std.foldl(
function(acc, key)
get(acc, key, default),
xtd.string.splitEscape(_path, '.'),
source,
),

'#convertBracketToDot':: d.fn(
|||
`convertBracketToDot` converts the bracket notation to dot notation.
This function does not support escaping brackets/quotes in path keys.
|||,
[
d.arg('path', d.T.string,),
]
),
convertBracketToDot(path):
if std.length(std.findSubstr('[', path)) > 0
then
local split = std.split(path, '[');
std.join('.', [
local a = std.stripChars(i, "[]'");
std.strReplace(a, '@.', '@\\.')
for i in split
])
else path,

local get(source, key, default) =
if key == ''
|| key == '$'
|| key == '*'
then source
else if std.isArray(source)
then getFromArray(source, key)
else std.get(source, key, default),

local getFromArray(arr, key) =
if std.startsWith(key, '?(@\\.')
then
std.filter(
self.parseFilterExpr(std.stripChars(key, '?(@\\.)')),
arr
)
else if std.length(std.findSubstr(':', key)) >= 1
then
local split = std.splitLimit(key, ':', 2);
local step =
if std.length(split) < 3
then 1
else parseIntOrNull(split[2]);
xtd.array.slice(
arr,
parseIntOrNull(split[0]),
parseIntOrNull(split[1]),
step,
)
else
arr[std.parseInt(key)],

local parseIntOrNull(str) =
if str == ''
then null
else std.parseInt(str),

'#parseFilterExpr':: d.fn(
|||
`parseFilterExpr` returns a filter function `f(x)` for a filter expression `expr`.
It supports comparisons (<, <=, >, >=) and equality checks (==, !=). If it doesn't
have an operator, it will check if the `expr` value exists.
|||,
[
d.arg('path', d.T.string,),
]
),
parseFilterExpr(expr):
local operandFunctions = {
'=='(a, b): a == b,
'!='(a, b): a != b,
'<='(a, b): a <= b,
'>='(a, b): a >= b,
'<'(a, b): a < b,
'>'(a, b): a > b,
};

local findOperands = std.filter(
function(op) std.length(std.findSubstr(op, expr)) > 0,
std.reverse( // reverse to match '<=' before '<'
std.objectFields(operandFunctions)
)
);

if std.length(findOperands) > 0
then
local op = findOperands[0];
local s = [
std.stripChars(i, ' ')
for i in std.splitLimit(expr, op, 1)
];
function(x)
if s[0] in x
then
local left = x[s[0]];
local right =
if std.isNumber(left)
then std.parseInt(s[1]) // Only parse if comparing numbers
else s[1];
operandFunctions[op](left, right)
else false
else
// Default to key matching
function(x) (expr in x),
}
5 changes: 4 additions & 1 deletion main.libsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ local d = import 'doc-util/main.libsonnet';
),

aggregate: (import './aggregate.libsonnet'),
array: (import './array.libsonnet'),
ascii: (import './ascii.libsonnet'),
date: (import './date.libsonnet'),
camelcase: (import './camelcase.libsonnet'),
date: (import './date.libsonnet'),
inspect: (import './inspect.libsonnet'),
jsonpath: (import './jsonpath.libsonnet'),
string: (import './string.libsonnet'),
url: (import './url.libsonnet'),
}
35 changes: 35 additions & 0 deletions string.libsonnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
local d = import 'doc-util/main.libsonnet';

{
'#': d.pkg(
name='string',
url='github.com/jsonnet-libs/xtd/string.libsonnet',
help='`string` implements helper functions for processing strings.',
),

// BelRune is a string of the Ascii character BEL which made computers ring in ancient times.
// We use it as "magic" char to temporarily replace an escaped string as it is a non printable
// character and thereby will unlikely be in a valid key by accident. Only when we include it.
local BelRune = std.char(7),

'#splitEscape':: d.fn(
|||
`split` works the same as `std.split` but with support for escaping the dividing
string `c`.
|||,
[
d.arg('str', d.T.string),
d.arg('c', d.T.string),
d.arg('escape', d.T.string, default='\\'),
]
),
splitEscape(str, c, escape='\\'):
std.map(
function(i)
std.strReplace(i, BelRune, escape + c),
std.split(
std.strReplace(str, escape + c, BelRune),
c,
)
),
}
Loading

0 comments on commit e425fcc

Please sign in to comment.