@peter_marklund/json 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -5
- package/bin/json.js +37 -1
- package/package.json +1 -1
- package/src/helpers.js +11 -4
- package/test/custom-helpers.js +20 -0
- package/test/input/array.json +17 -0
- package/test/input/array.jsonl +3 -0
- package/test/run.js +2 -1
- package/test/test_spec.json +21 -1
package/README.md
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
# @peter_marklund/json
|
|
2
2
|
|
|
3
|
-
An npm package that provides a convenient way to work with JSON
|
|
4
|
-
|
|
5
|
-
## TODO
|
|
6
|
-
|
|
7
|
-
* Use stable/sorted JSON stringify
|
|
3
|
+
An npm package that provides a convenient way to use JavaScript to work with JSON in the terminal. This tool is for those of us who like `jq` but prefer JavaScript over `jq` syntax.
|
|
8
4
|
|
|
9
5
|
## Installation
|
|
10
6
|
|
|
@@ -14,8 +10,138 @@ npm install @peter_marklund/json -g
|
|
|
14
10
|
|
|
15
11
|
## Usage
|
|
16
12
|
|
|
13
|
+
The JSON data is typically passed to the `json` command via stdin but can also be passed as a file path via the second argument. The first argument to the `json` command is a string with JavaScript code to be evaluated. All [lodash](https://lodash.com/docs/4.17.23) functions (i.e. `pick`, `pickBy`, `mapValues`, `sum` etc.) are available as are a number of [helper functions](src/helpers.js). It is also possible to provide custom JavaScript helper functions via the `JSON_HELPERS_PATH` environment variable.
|
|
14
|
+
|
|
17
15
|
```sh
|
|
16
|
+
# Get the value at a path
|
|
18
17
|
echo '{"foo": "1"}' | json .foo
|
|
18
|
+
|
|
19
|
+
# Get the keys of the JSON data
|
|
20
|
+
cat test/input/basic.json | json 'Object.keys(data)'
|
|
21
|
+
# [
|
|
22
|
+
# "foo",
|
|
23
|
+
# "bar",
|
|
24
|
+
# "baz",
|
|
25
|
+
# "nested",
|
|
26
|
+
# "data"
|
|
27
|
+
# ]
|
|
28
|
+
|
|
29
|
+
# Get the length of an array:
|
|
30
|
+
cat test/input/basic.json | json '.data.length'
|
|
31
|
+
# 3
|
|
32
|
+
|
|
33
|
+
# Use lodash functions
|
|
34
|
+
cat test/input/basic.json | json '.data.map(d => pick(d, ["value"]))'
|
|
35
|
+
# [
|
|
36
|
+
# {
|
|
37
|
+
# "value": 100
|
|
38
|
+
# },
|
|
39
|
+
# {
|
|
40
|
+
# "value": 200
|
|
41
|
+
# },
|
|
42
|
+
# {
|
|
43
|
+
# "value": 300
|
|
44
|
+
# }
|
|
45
|
+
# ]
|
|
46
|
+
|
|
47
|
+
# Use the flattenJson helper to find the path of a deeply nested value:
|
|
48
|
+
cat test/input/basic.json | json 'flattenJson(data)'
|
|
49
|
+
# {
|
|
50
|
+
# "bar": "Hello world",
|
|
51
|
+
# "baz": false,
|
|
52
|
+
# "data.0.id": 1,
|
|
53
|
+
# "data.0.name": "Item 1",
|
|
54
|
+
# "data.0.value": 100,
|
|
55
|
+
# "data.1.id": 2,
|
|
56
|
+
# "data.1.name": "Item 2",
|
|
57
|
+
# "data.1.value": 200,
|
|
58
|
+
# "data.2.id": 3,
|
|
59
|
+
# "data.2.name": "Item 3",
|
|
60
|
+
# "data.2.value": 300,
|
|
61
|
+
# "foo": 1,
|
|
62
|
+
# "nested.foo.bar": "nested value"
|
|
63
|
+
# }
|
|
64
|
+
|
|
65
|
+
# Use the stats helper function to get min/max/avg/median/p90 etc. for numerical values
|
|
66
|
+
cat test/input/basic.json | json 'data.data.map(d => d.value)' | json 'stats(data)'
|
|
67
|
+
# {
|
|
68
|
+
# "avg": 200,
|
|
69
|
+
# "count": 3,
|
|
70
|
+
# "max": 300,
|
|
71
|
+
# "min": 100,
|
|
72
|
+
# "p1": 102,
|
|
73
|
+
# "p10": 120,
|
|
74
|
+
# "p20": 140,
|
|
75
|
+
# "p30": 160,
|
|
76
|
+
# "p40": 180,
|
|
77
|
+
# "p5": 110,
|
|
78
|
+
# "p50": 200,
|
|
79
|
+
# "p60": 220,
|
|
80
|
+
# "p70": 240,
|
|
81
|
+
# "p80": 260,
|
|
82
|
+
# "p90": 280,
|
|
83
|
+
# "p95": 290,
|
|
84
|
+
# "p99": 298,
|
|
85
|
+
# "p999": 299.79999999999995,
|
|
86
|
+
# "stdDev": 81.64965809277261,
|
|
87
|
+
# "sum": 600
|
|
88
|
+
# }
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# Colorized pretty printing is the default
|
|
92
|
+
cat test/input/array.json | json
|
|
93
|
+
# [
|
|
94
|
+
# {
|
|
95
|
+
# "id": 1,
|
|
96
|
+
# "name": "Item 1",
|
|
97
|
+
# "value": 100
|
|
98
|
+
# },
|
|
99
|
+
# {
|
|
100
|
+
# "id": 2,
|
|
101
|
+
# "name": "Item 2",
|
|
102
|
+
# "value": 200
|
|
103
|
+
# },
|
|
104
|
+
# {
|
|
105
|
+
# "id": 3,
|
|
106
|
+
# "name": "Item 3",
|
|
107
|
+
# "value": 300
|
|
108
|
+
# }
|
|
109
|
+
# ]
|
|
110
|
+
|
|
111
|
+
# Without pretty printing (single line):
|
|
112
|
+
cat test/input/basic.json | PRETTY=false json
|
|
113
|
+
# {"bar":"Hello world","baz":false,"data":[{"id":1,"name":"Item 1","value":100},{"id":2,"name":"Item 2","value":200},{"id":3,"name":"Item 3","value":300}],"foo":1,"nested":{"foo":{"bar":"nested value"}}}
|
|
114
|
+
|
|
115
|
+
# JSONL output (for an array with one JSON object per line)
|
|
116
|
+
cat test/input/array.json | JSONL=true json
|
|
117
|
+
# {"id":1,"name":"Item 1","value":100}
|
|
118
|
+
# {"id":2,"name":"Item 2","value":200}
|
|
119
|
+
# {"id":3,"name":"Item 3","value":300}
|
|
120
|
+
|
|
121
|
+
# The json command can take JSONL as input as well
|
|
122
|
+
cat test/input/array.jsonl | json
|
|
123
|
+
|
|
124
|
+
# The json command can also parse JSON data at the end of log lines:
|
|
125
|
+
cat test/input/log-with-json.log | json
|
|
126
|
+
# [
|
|
127
|
+
# {
|
|
128
|
+
# "_line": "192.168.1.1 - - [21/Feb/2026:10:00:01 +0000] \"GET /api/users HTTP/1.1\" 200 ",
|
|
129
|
+
# "cache": "hit",
|
|
130
|
+
# "duration_ms": 42,
|
|
131
|
+
# "user_id": 1021
|
|
132
|
+
# },
|
|
133
|
+
# {
|
|
134
|
+
# "_line": "192.168.1.2 - - [21/Feb/2026:10:00:03 +0000] \"POST /api/orders HTTP/1.1\" 201 ",
|
|
135
|
+
# "cache": "miss",
|
|
136
|
+
# "duration_ms": 87,
|
|
137
|
+
# "user_id": 4432
|
|
138
|
+
# },
|
|
139
|
+
# ...
|
|
140
|
+
# ]
|
|
141
|
+
|
|
142
|
+
# Using custom helper functions via the JSON_HELPERS_PATH env var and a javascript module with exported functions
|
|
143
|
+
echo '{"values1": [1, 2, 3, 4], "values2": [3, 5, 1, 11]}' | JSON_HELPERS_PATH="$(pwd)/test/custom-helpers.js" json 'correlation(data.values1, data.values2)'
|
|
144
|
+
# 0.5976143046671968
|
|
19
145
|
```
|
|
20
146
|
|
|
21
147
|
## Running the Tests
|
|
@@ -35,4 +161,5 @@ npm publish --access public
|
|
|
35
161
|
|
|
36
162
|
# Prior Art
|
|
37
163
|
|
|
164
|
+
* [jq](https://github.com/jqlang/jq) - the standard for processing JSON in the terminal
|
|
38
165
|
* [trentm/json](https://github.com/trentm/json) - nice library with [very good documentation](https://trentm.com/json/)
|
package/bin/json.js
CHANGED
|
@@ -40,9 +40,14 @@
|
|
|
40
40
|
|
|
41
41
|
const fs = require("fs");
|
|
42
42
|
const readline = require("readline");
|
|
43
|
+
|
|
43
44
|
const _ = require("lodash");
|
|
44
45
|
Object.assign(global, require("lodash"));
|
|
45
46
|
Object.assign(global, require("../src/helpers.js"));
|
|
47
|
+
if (process.env.JSON_HELPERS_PATH) {
|
|
48
|
+
Object.assign(global, require(process.env.JSON_HELPERS_PATH));
|
|
49
|
+
}
|
|
50
|
+
|
|
46
51
|
const { diff } = require("object-diffy");
|
|
47
52
|
const { colorize } = require("json-colorizer");
|
|
48
53
|
const stringify = require('fast-json-stable-stringify')
|
|
@@ -61,7 +66,38 @@ function getCodeArg() {
|
|
|
61
66
|
function readStdIn() {
|
|
62
67
|
return fs.readFileSync(0).toString();
|
|
63
68
|
}
|
|
64
|
-
|
|
69
|
+
|
|
70
|
+
function parseLine(line, openLines) {
|
|
71
|
+
let result = { openLines: [...openLines] };
|
|
72
|
+
try {
|
|
73
|
+
const openIndex = line.startsWith("[") ? 0 : line.indexOf("{");
|
|
74
|
+
if (
|
|
75
|
+
openLines.length === 0 &&
|
|
76
|
+
((line.indexOf("{") >= 0 && line.endsWith("}")) ||
|
|
77
|
+
(line.startsWith("[") && line.endsWith("]")))
|
|
78
|
+
) {
|
|
79
|
+
const doc = JSON.parse(line.substring(openIndex));
|
|
80
|
+
if (openIndex > 0 && !doc._line) doc._line = line.substring(0, openIndex);
|
|
81
|
+
result.parsedLine = doc;
|
|
82
|
+
} else if (line === "{") {
|
|
83
|
+
result.openLines = [line];
|
|
84
|
+
} else if (openLines.length > 0) {
|
|
85
|
+
result.openLines.push(line);
|
|
86
|
+
if (line === "}") {
|
|
87
|
+
result.parsedLine = JSON.parse(openLines.join("\n"));
|
|
88
|
+
result.openLines = [];
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
result.parsedLine = { _line: line };
|
|
92
|
+
}
|
|
93
|
+
} catch (err) {
|
|
94
|
+
result.error = `Error thrown parsing line: ${line} - ${err.stack}`;
|
|
95
|
+
result.openLines = [];
|
|
96
|
+
result.parsedLine = { _line: line };
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
65
101
|
async function jsonIn(filePath) {
|
|
66
102
|
let textInput
|
|
67
103
|
try {
|
package/package.json
CHANGED
package/src/helpers.js
CHANGED
|
@@ -54,10 +54,17 @@ function base64Encode(data) {
|
|
|
54
54
|
return data && Buffer.from(data).toString("base64");
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
function percentile(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
function percentile(arr, p) {
|
|
58
|
+
if (typeof p !== 'number' || p <= 0 || p >= 1.0) {
|
|
59
|
+
throw new Error('Percentile must be a number between 0 and 1');
|
|
60
|
+
}
|
|
61
|
+
const sorted = [...arr].sort((a, b) => a - b);
|
|
62
|
+
const index = p * (sorted.length - 1);
|
|
63
|
+
const lower = Math.floor(index);
|
|
64
|
+
const upper = Math.ceil(index);
|
|
65
|
+
const weight = index - lower;
|
|
66
|
+
|
|
67
|
+
return sorted[lower] * (1 - weight) + sorted[upper] * weight;
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
function p50(values) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
function correlation(x, y) {
|
|
2
|
+
const n = x.length;
|
|
3
|
+
const meanX = x.reduce((a, b) => a + b, 0) / n;
|
|
4
|
+
const meanY = y.reduce((a, b) => a + b, 0) / n;
|
|
5
|
+
|
|
6
|
+
let num = 0, denomX = 0, denomY = 0;
|
|
7
|
+
for (let i = 0; i < n; i++) {
|
|
8
|
+
const dx = x[i] - meanX;
|
|
9
|
+
const dy = y[i] - meanY;
|
|
10
|
+
num += dx * dy;
|
|
11
|
+
denomX += dx * dx;
|
|
12
|
+
denomY += dy * dy;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return num / Math.sqrt(denomX * denomY);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
correlation,
|
|
20
|
+
}
|
package/test/run.js
CHANGED
|
@@ -25,7 +25,7 @@ async function runTest(test) {
|
|
|
25
25
|
console.log(`output: ${output}`)
|
|
26
26
|
const elapsedTime = Date.now() - startTime
|
|
27
27
|
console.log(`elapsed: ${elapsedTime}`)
|
|
28
|
-
if (isJsonScalarValue(test.expected)) {
|
|
28
|
+
if (isJsonScalarValue(test.expected) || test.command.includes('JSONL=true')) {
|
|
29
29
|
assert.strictEqual(output, test.expected)
|
|
30
30
|
} else {
|
|
31
31
|
assert.strictEqual(stringify(JSON.parse(output)), stringify(JSON.parse(test.expected)))
|
|
@@ -42,6 +42,7 @@ async function run() {
|
|
|
42
42
|
const elapsedTime = Date.now() - startTime
|
|
43
43
|
console.log(`\nTotal elapsed: ${elapsedTime}`)
|
|
44
44
|
console.log(`Total tests run: ${spec.tests.length}`)
|
|
45
|
+
console.log('SUCCESS!')
|
|
45
46
|
process.exit(0)
|
|
46
47
|
} catch (error) {
|
|
47
48
|
console.log(error.stack || error)
|
package/test/test_spec.json
CHANGED
|
@@ -48,7 +48,27 @@
|
|
|
48
48
|
{
|
|
49
49
|
"name": "stats",
|
|
50
50
|
"command": "cat test/input/basic.json | json 'pick(stats(data.data.map(i => i.value)), [\"min\", \"max\", \"avg\", \"p50\", \"p90\"])'",
|
|
51
|
-
"expected": "{\"min\": 100, \"max\": 300, \"avg\": 200, \"p50\": 200, \"p90\":
|
|
51
|
+
"expected": "{\"min\": 100, \"max\": 300, \"avg\": 200, \"p50\": 200, \"p90\": 280}"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "jsonl_input",
|
|
55
|
+
"command": "cat test/input/array.jsonl | json",
|
|
56
|
+
"expected": "[{\"id\":1,\"name\":\"Item 1\",\"value\":100},{\"id\":2,\"name\":\"Item 2\",\"value\":200},{\"id\":3,\"name\":\"Item 3\",\"value\":300}]"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "jsonl_output",
|
|
60
|
+
"command": "cat test/input/array.jsonl | JSONL=true json",
|
|
61
|
+
"expected": "{\"id\":1,\"name\":\"Item 1\",\"value\":100}\n{\"id\":2,\"name\":\"Item 2\",\"value\":200}\n{\"id\":3,\"name\":\"Item 3\",\"value\":300}"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"name": "log_with_json_input",
|
|
65
|
+
"command": "cat test/input/log-with-json.log | json 'sum(data.map(d => d.duration_ms))'",
|
|
66
|
+
"expected": "141"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "custom_helper_functions",
|
|
70
|
+
"command": "echo '{\"values1\": [1, 2, 3, 4], \"values2\": [3, 5, 1, 11]}' | JSON_HELPERS_PATH=\"$(pwd)/test/custom-helpers.js\" json 'round(correlation(data.values1, data.values2), 3)'",
|
|
71
|
+
"expected": "0.598"
|
|
52
72
|
}
|
|
53
73
|
]
|
|
54
74
|
}
|