Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add jsonpath lib (and related helpers) #15

Merged
merged 4 commits into from
Jun 2, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've got a PR open to upstream this: google/jsonnet#1093

[
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`.
139 changes: 139 additions & 0 deletions jsonpath.libsonnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
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,),
]
),
getJSONPath(source, path):
local _path = self.convertBracketToDot(path);
std.foldl(
function(acc, key)
get(acc, key),
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) =
if key == ''
|| key == '$'
|| key == '*'
then source
else if std.isArray(source)
then getFromArray(source, key)
else source[key],

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