@peter_marklund/json 0.0.3 → 0.0.5

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 CHANGED
@@ -16,17 +16,71 @@ npm install @peter_marklund/json -g
16
16
 
17
17
  ```sh
18
18
  echo '{"foo": "1"}' | json .foo
19
+
20
+ # Colorized pretty printing is the default
21
+ cat test/input/array.json | json
22
+ # [
23
+ # {
24
+ # "id": 1,
25
+ # "name": "Item 1",
26
+ # "value": 100
27
+ # },
28
+ # {
29
+ # "id": 2,
30
+ # "name": "Item 2",
31
+ # "value": 200
32
+ # },
33
+ # {
34
+ # "id": 3,
35
+ # "name": "Item 3",
36
+ # "value": 300
37
+ # }
38
+ # ]
39
+
40
+ # Without pretty printing (single line):
41
+ cat test/input/basic.json | PRETTY=false json
42
+ # {"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"}}}
43
+
44
+ # JSONL output (for an array with one JSON object per line)
45
+ cat test/input/array.json | JSONL=true json
46
+ # {"id":1,"name":"Item 1","value":100}
47
+ # {"id":2,"name":"Item 2","value":200}
48
+ # {"id":3,"name":"Item 3","value":300}
49
+
50
+ # The json command can take JSONL as input as well
51
+ cat test/input/array.jsonl | json
52
+
53
+ # The json command can also parse JSON data at the end of log lines:
54
+ cat test/input/log-with-json.log | json
55
+ # [
56
+ # {
57
+ # "_line": "192.168.1.1 - - [21/Feb/2026:10:00:01 +0000] \"GET /api/users HTTP/1.1\" 200 ",
58
+ # "cache": "hit",
59
+ # "duration_ms": 42,
60
+ # "user_id": 1021
61
+ # },
62
+ # {
63
+ # "_line": "192.168.1.2 - - [21/Feb/2026:10:00:03 +0000] \"POST /api/orders HTTP/1.1\" 201 ",
64
+ # "cache": "miss",
65
+ # "duration_ms": 87,
66
+ # "user_id": 4432
67
+ # },
68
+ # ...
69
+ # ]
19
70
  ```
20
71
 
21
- ## Developing the Library Locally
72
+ ## Running the Tests
22
73
 
23
74
  ```sh
75
+ npm install
24
76
  npm link
77
+ npm test
25
78
  ```
26
79
 
27
80
  ## Publishing a new Version
28
81
 
29
82
  ```sh
83
+ npm login
30
84
  npm publish --access public
31
85
  ```
32
86
 
package/bin/json.js CHANGED
@@ -42,8 +42,10 @@ const fs = require("fs");
42
42
  const readline = require("readline");
43
43
  const _ = require("lodash");
44
44
  Object.assign(global, require("lodash"));
45
+ Object.assign(global, require("../src/helpers.js"));
45
46
  const { diff } = require("object-diffy");
46
47
  const { colorize } = require("json-colorizer");
48
+ const stringify = require('fast-json-stable-stringify')
47
49
 
48
50
  function getCodeArg() {
49
51
  let code = process.argv[2] || "data"
@@ -59,12 +61,43 @@ function getCodeArg() {
59
61
  function readStdIn() {
60
62
  return fs.readFileSync(0).toString();
61
63
  }
62
-
64
+
65
+ function parseLine(line, openLines) {
66
+ let result = { openLines: [...openLines] };
67
+ try {
68
+ const openIndex = line.startsWith("[") ? 0 : line.indexOf("{");
69
+ if (
70
+ openLines.length === 0 &&
71
+ ((line.indexOf("{") >= 0 && line.endsWith("}")) ||
72
+ (line.startsWith("[") && line.endsWith("]")))
73
+ ) {
74
+ const doc = JSON.parse(line.substring(openIndex));
75
+ if (openIndex > 0 && !doc._line) doc._line = line.substring(0, openIndex);
76
+ result.parsedLine = doc;
77
+ } else if (line === "{") {
78
+ result.openLines = [line];
79
+ } else if (openLines.length > 0) {
80
+ result.openLines.push(line);
81
+ if (line === "}") {
82
+ result.parsedLine = JSON.parse(openLines.join("\n"));
83
+ result.openLines = [];
84
+ }
85
+ } else {
86
+ result.parsedLine = { _line: line };
87
+ }
88
+ } catch (err) {
89
+ result.error = `Error thrown parsing line: ${line} - ${err.stack}`;
90
+ result.openLines = [];
91
+ result.parsedLine = { _line: line };
92
+ }
93
+ return result;
94
+ }
95
+
63
96
  async function jsonIn(filePath) {
64
97
  let textInput
65
98
  try {
66
99
  if (filePath) {
67
- return require(filePath)
100
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'))
68
101
  } else {
69
102
  textInput = readStdIn()
70
103
  // NOTE: I've found JSON.parse intermittently errors out for data sizes around 15 MB but require(filePath) can handle more?
@@ -119,12 +152,18 @@ async function jsonIn(filePath) {
119
152
  }
120
153
  }
121
154
 
155
+ function printJsonLines(data) {
156
+ for (const line of data) {
157
+ console.log(stringify(line));
158
+ }
159
+ }
160
+
122
161
  function printJson(data) {
123
- console.log(JSON.stringify(data))
162
+ console.log(stringify(data))
124
163
  }
125
-
164
+
126
165
  function printPrettyJson(data) {
127
- console.log(colorize(JSON.stringify(data, null, 4)))
166
+ console.log(colorize(stringify(data, null, 4)))
128
167
  }
129
168
 
130
169
  async function main() {
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@peter_marklund/json",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "A convenient way to work with JSON using JavaScript in the terminal",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
7
+ "test": "./test/run.js"
8
8
  },
9
9
  "repository": {
10
10
  "type": "git",
@@ -22,6 +22,7 @@
22
22
  },
23
23
  "homepage": "https://github.com/peter/json#readme",
24
24
  "dependencies": {
25
+ "fast-json-stable-stringify": "^2.1.0",
25
26
  "json-colorizer": "^3.0.1",
26
27
  "lodash": "^4.17.23",
27
28
  "object-diffy": "^1.0.4"
package/src/helpers.js ADDED
@@ -0,0 +1,120 @@
1
+ function flattenJson(data, path = []) {
2
+ if (Array.isArray(data)) {
3
+ return data.reduce((acc, value, index) => {
4
+ const valueJson = flattenJson(value, [...path, index]);
5
+ for (const [key, value] of Object.entries(valueJson)) {
6
+ acc[key] = value;
7
+ }
8
+ return acc;
9
+ }, {});
10
+ } else if (typeof data === "object" && data !== null) {
11
+ return Object.keys(data).reduce((acc, key) => {
12
+ const keyJson = flattenJson(data[key], [...path, key]);
13
+ for (const [key, value] of Object.entries(keyJson)) {
14
+ acc[key] = value;
15
+ }
16
+ return acc;
17
+ }, {});
18
+ } else {
19
+ return { [path.join(".")]: data };
20
+ }
21
+ }
22
+
23
+ // https://stackoverflow.com/questions/1248302/how-to-get-the-size-of-a-javascript-object
24
+ function sizeOfObject(object) {
25
+ var objectList = [];
26
+ var stack = [object];
27
+ var bytes = 0;
28
+
29
+ while (stack.length) {
30
+ var value = stack.pop();
31
+
32
+ if (typeof value === "boolean") {
33
+ bytes += 4;
34
+ } else if (typeof value === "string") {
35
+ bytes += value.length * 2;
36
+ } else if (typeof value === "number") {
37
+ bytes += 8;
38
+ } else if (typeof value === "object" && objectList.indexOf(value) === -1) {
39
+ objectList.push(value);
40
+
41
+ for (var i in value) {
42
+ stack.push(value[i]);
43
+ }
44
+ }
45
+ }
46
+ return bytes;
47
+ }
48
+
49
+ function base64Decode(data) {
50
+ return data && Buffer.from(data, "base64").toString();
51
+ }
52
+
53
+ function base64Encode(data) {
54
+ return data && Buffer.from(data).toString("base64");
55
+ }
56
+
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;
68
+ }
69
+
70
+ function p50(values) {
71
+ return percentile(values, 0.5);
72
+ }
73
+
74
+ function p90(values) {
75
+ return percentile(values, 0.9);
76
+ }
77
+
78
+ function p99(values) {
79
+ return percentile(values, 0.99);
80
+ }
81
+
82
+ function stdDev(values, avg) {
83
+ const sum = values.reduce((acc, v) => acc + (v - avg) ** 2, 0);
84
+ return Math.sqrt(sum / values.length);
85
+ }
86
+
87
+ function stats(values) {
88
+ const sum = values.reduce((acc, v) => acc + v, 0);
89
+ const avg = sum / values.length;
90
+ return {
91
+ count: values.length,
92
+ min: Math.min(...values),
93
+ max: Math.max(...values),
94
+ stdDev: stdDev(values, avg),
95
+ sum,
96
+ avg,
97
+ p1: percentile(values, 0.01),
98
+ p5: percentile(values, 0.05),
99
+ p10: percentile(values, 0.1),
100
+ p20: percentile(values, 0.2),
101
+ p30: percentile(values, 0.3),
102
+ p40: percentile(values, 0.4),
103
+ p50: percentile(values, 0.5),
104
+ p60: percentile(values, 0.6),
105
+ p70: percentile(values, 0.7),
106
+ p80: percentile(values, 0.8),
107
+ p90: percentile(values, 0.9),
108
+ p95: percentile(values, 0.95),
109
+ p99: percentile(values, 0.99),
110
+ p999: percentile(values, 0.999),
111
+ };
112
+ }
113
+
114
+ module.exports = {
115
+ flattenJson,
116
+ sizeOfObject,
117
+ base64Decode,
118
+ base64Encode,
119
+ stats,
120
+ }
@@ -0,0 +1,17 @@
1
+ [
2
+ {
3
+ "id": 1,
4
+ "name": "Item 1",
5
+ "value": 100
6
+ },
7
+ {
8
+ "id": 2,
9
+ "name": "Item 2",
10
+ "value": 200
11
+ },
12
+ {
13
+ "id": 3,
14
+ "name": "Item 3",
15
+ "value": 300
16
+ }
17
+ ]
@@ -0,0 +1,3 @@
1
+ {"id":1,"name":"Item 1","value":100}
2
+ {"id":2,"name":"Item 2","value":200}
3
+ {"id":3,"name":"Item 3","value":300}
@@ -0,0 +1,27 @@
1
+ {
2
+ "foo": 1,
3
+ "bar": "Hello world",
4
+ "baz": false,
5
+ "nested": {
6
+ "foo": {
7
+ "bar": "nested value"
8
+ }
9
+ },
10
+ "data": [
11
+ {
12
+ "id": 1,
13
+ "name": "Item 1",
14
+ "value": 100
15
+ },
16
+ {
17
+ "id": 2,
18
+ "name": "Item 2",
19
+ "value": 200
20
+ },
21
+ {
22
+ "id": 3,
23
+ "name": "Item 3",
24
+ "value": 300
25
+ }
26
+ ]
27
+ }
package/test/run.js ADDED
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+ const childProcess = require('child_process')
6
+ const { promisify } = require('util')
7
+ const assert = require('node:assert')
8
+ const stringify = require('fast-json-stable-stringify')
9
+
10
+ const exec = (cmd) => promisify(childProcess.exec)(cmd).then(result => result.stdout.trim())
11
+
12
+ // Is the input a JSON string, number, boolean, or null (i.e. not an object or array)
13
+ function isJsonScalarValue(text) {
14
+ // return text && (text === 'null' || text === 'true' || text === 'false' || text.match(/^\d+$/) || (text.startsWith('"') && text.endsWith('"')))
15
+ return !(text.startsWith('{') || text.startsWith('['))
16
+ }
17
+
18
+ async function runTest(test) {
19
+ const startTime = Date.now()
20
+ console.log('\n--------------------------------------')
21
+ console.log(`Test: ${test.name}`)
22
+ console.log('--------------------------------------\n')
23
+ console.log(`command: ${test.command}`)
24
+ const output = await exec(test.command)
25
+ console.log(`output: ${output}`)
26
+ const elapsedTime = Date.now() - startTime
27
+ console.log(`elapsed: ${elapsedTime}`)
28
+ if (isJsonScalarValue(test.expected) || test.command.includes('JSONL=true')) {
29
+ assert.strictEqual(output, test.expected)
30
+ } else {
31
+ assert.strictEqual(stringify(JSON.parse(output)), stringify(JSON.parse(test.expected)))
32
+ }
33
+ }
34
+
35
+ async function run() {
36
+ try {
37
+ const startTime = Date.now()
38
+ const spec = JSON.parse(fs.readFileSync(path.join(__dirname, 'test_spec.json'), 'utf8'))
39
+ for (const test of spec.tests) {
40
+ const result = await runTest(test)
41
+ }
42
+ const elapsedTime = Date.now() - startTime
43
+ console.log(`\nTotal elapsed: ${elapsedTime}`)
44
+ console.log(`Total tests run: ${spec.tests.length}`)
45
+ console.log('SUCCESS!')
46
+ process.exit(0)
47
+ } catch (error) {
48
+ console.log(error.stack || error)
49
+ console.log('FAILURE!')
50
+ process.exit(1)
51
+ }
52
+ }
53
+
54
+ run()
@@ -0,0 +1,69 @@
1
+ {
2
+ "tests": [
3
+ {
4
+ "name": "echo_no_arg",
5
+ "command": "echo '{\"foo\": 1}' | json",
6
+ "expected": "{\"foo\": 1}"
7
+ },
8
+ {
9
+ "name": "echo_dot",
10
+ "command": "echo '{\"foo\": 1}' | json .",
11
+ "expected": "{\"foo\": 1}"
12
+ },
13
+ {
14
+ "name": "echo_data",
15
+ "command": "echo '{\"foo\": 1}' | json data",
16
+ "expected": "{\"foo\": 1}"
17
+ },
18
+ {
19
+ "name": "get_path",
20
+ "command": "cat test/input/basic.json | json '.nested?.foo?.bar'",
21
+ "expected": "nested value"
22
+ },
23
+ {
24
+ "name": "file_path_arg",
25
+ "command": "json .foo test/input/basic.json",
26
+ "expected": "1"
27
+ },
28
+ {
29
+ "name": "lodash_sum",
30
+ "command": "cat test/input/basic.json | json 'sum(data.data.map(i => i.value))'",
31
+ "expected": "600"
32
+ },
33
+ {
34
+ "name": "object_keys",
35
+ "command": "cat test/input/basic.json | json 'Object.keys(data)'",
36
+ "expected": "[\"foo\", \"bar\", \"baz\", \"nested\", \"data\"]"
37
+ },
38
+ {
39
+ "name": "array_length",
40
+ "command": "cat test/input/basic.json | json '.data.length'",
41
+ "expected": "3"
42
+ },
43
+ {
44
+ "name": "flatten_json",
45
+ "command": "cat test/input/basic.json | json 'flattenJson(data)'",
46
+ "expected": "{\"foo\": 1, \"bar\": \"Hello world\", \"baz\": false, \"nested.foo.bar\": \"nested value\", \"data.0.id\": 1, \"data.0.name\": \"Item 1\", \"data.0.value\": 100, \"data.1.id\": 2, \"data.1.name\": \"Item 2\", \"data.1.value\": 200, \"data.2.id\": 3, \"data.2.name\": \"Item 3\", \"data.2.value\": 300}"
47
+ },
48
+ {
49
+ "name": "stats",
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\": 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
+ }