Add own JSON parser

pull/251/head
Anton Medvedev 1 year ago
parent 36ca6f12ee
commit 8d4292a41b
No known key found for this signature in database

@ -1,6 +1,7 @@
# fx
The JavaScript version of the **fx**. Short for _Function eXecution_ or _f(x)_.
A non-interactive, JavaScript version of the [**fx**](https://fx.wtf).
Short for _Function eXecution_ or _f(x)_.
```sh
npm i -g fx

@ -8,49 +8,47 @@ void async function main() {
return
}
let input = ''
process.stdin.setEncoding('utf8')
for await (const chunk of process.stdin)
input += chunk
let json
if (['-r', '--raw'].includes(args[0])) {
args.shift()
json = input
} else try {
json = JSON.parse(input)
} catch (err) {
process.stderr.write(`Invalid JSON: ${err.message}\n`)
return process.exitCode = 1
}
let i, code, output = json
for ([i, code] of args.entries()) try {
output = await transform(output, code)
} catch (err) {
printErr(err)
return process.exitCode = 1
}
if (typeof output === 'undefined')
process.stderr.write('undefined\n')
else if (typeof output === 'string')
console.log(output)
else
console.log(JSON.stringify(output, null, 2))
function printErr(err) {
let pre = args.slice(0, i).join(' ')
let post = args.slice(i + 1).join(' ')
if (pre.length > 20) pre = '...' + pre.substring(pre.length - 20)
if (post.length > 20) post = post.substring(0, 20) + '...'
process.stderr.write(
`\n ${pre} ${code} ${post}\n` +
` ${' '.repeat(pre.length + 1)}${'^'.repeat(code.length)}\n` +
`\n${err.stack || err}\n`
)
}
}()
JSON.stringify = value => stringify(value)
for await (const json of parseJson()) {
// let json
// if (['-r', '--raw'].includes(args[0])) {
// args.shift()
// json = input
// } else try {
// json = JSON.parse(input)
// } catch (err) {
// process.stderr.write(`Invalid JSON: ${err.message}\n`)
// return process.exitCode = 1
// }
let i, code, output = json
for ([i, code] of args.entries()) try {
output = await transform(output, code)
} catch (err) {
printErr(err)
return 1
}
if (typeof output === 'undefined')
process.stderr.write('undefined\n')
else if (typeof output === 'string')
console.log(output)
else
console.log(stringify(output, true))
function printErr(err) {
let pre = args.slice(0, i).join(' ')
let post = args.slice(i + 1).join(' ')
if (pre.length > 20) pre = '...' + pre.substring(pre.length - 20)
if (post.length > 20) post = post.substring(0, 20) + '...'
process.stderr.write(
`\n ${pre} ${code} ${post}\n` +
` ${' '.repeat(pre.length + 1)}${'^'.repeat(code.length)}\n` +
`\n${err.stack || err}\n`
)
}
}
}().then(exitCode => process.exitCode = exitCode)
async function transform(json, code) {
if ('.' === code)
@ -125,6 +123,327 @@ function groupBy(keyOrFunction) {
}
}
async function* stdin() {
const process = await import('node:process')
process.stdin.setEncoding('utf8')
for await (const chunk of process.stdin)
for (const ch of chunk)
yield ch
}
async function* parseJson() {
const gen = stdin()
let pos = 0, buffer = '', lastChar, done
async function next() {
({value: lastChar, done: done} = await gen.next())
pos++
buffer = buffer.slice(-10) + lastChar
}
function printErrorSnippet() {
const snippet = buffer + lastChar
let nextChars = ''
for (let i = 0; i < 10; i++) {
const {value: ch} = gen.next()
nextChars += ch || ''
}
return `Error snippet: ${snippet}${nextChars}`
}
await next()
while (!done) {
const value = await parseValue()
expectValue(value)
yield value
}
async function parseValue() {
await skipWhitespace()
const value =
await parseString() ??
await parseNumber() ??
await parseObject() ??
await parseArray() ??
await parseKeyword('true', true) ??
await parseKeyword('false', false) ??
await parseKeyword('null', null)
await skipWhitespace()
return value
}
async function parseString() {
if (lastChar !== '"') return
let str = ''
let escaped = false
while (true) {
await next()
if (escaped) {
if (lastChar === 'u') {
let unicode = ''
for (let i = 0; i < 4; i++) {
await next()
if (!isHexDigit(lastChar)) {
throw new SyntaxError(`Invalid Unicode escape sequence '\\u${unicode}${lastChar}' at position ${pos}`)
}
unicode += lastChar
}
str += String.fromCharCode(parseInt(unicode, 16))
} else {
const escapedChar = {
'"': '"',
'\\': '\\',
'/': '/',
'b': '\b',
'f': '\f',
'n': '\n',
'r': '\r',
't': '\t'
}[lastChar]
if (!escapedChar) {
throw new SyntaxError(`Invalid escape sequence '\\${lastChar}' at position ${pos}`)
}
str += escapedChar
}
escaped = false
} else if (lastChar === '\\') {
escaped = true
} else if (lastChar === '"') {
break
} else if (lastChar === undefined) {
throw new SyntaxError(`Unterminated string literal at position ${pos}`)
} else {
str += lastChar
}
}
await next()
return str
}
async function parseNumber() {
if (!isDigit(lastChar) && lastChar !== '-') return
let numStr = ''
if (lastChar === '-') {
numStr += lastChar
await next()
if (!isDigit(lastChar)) {
throw new SyntaxError(`Invalid number format at position ${pos}.`)
}
}
if (lastChar === '0') {
numStr += lastChar
await next()
} else {
while (isDigit(lastChar)) {
numStr += lastChar
await next()
}
}
if (lastChar === '.') {
numStr += lastChar
await next()
if (!isDigit(lastChar)) {
throw new SyntaxError(`Invalid number format at position ${pos}.`)
}
while (isDigit(lastChar)) {
numStr += lastChar
await next()
}
}
if (lastChar === 'e' || lastChar === 'E') {
numStr += lastChar
await next()
if (lastChar === '+' || lastChar === '-') {
numStr += lastChar
await next()
}
if (!isDigit(lastChar)) {
throw new SyntaxError(`Invalid number format at position ${pos}.`)
}
while (isDigit(lastChar)) {
numStr += lastChar
await next()
}
}
return isInteger(numStr) ? toSafeNumber(numStr) : parseFloat(numStr)
}
async function parseObject() {
if (lastChar !== '{') return
await next()
await skipWhitespace()
const obj = {}
if (lastChar === '}') {
await next()
return obj
}
while (true) {
if (lastChar !== '"') {
throw new SyntaxError(`Unexpected character '${lastChar}' at position ${pos}. Expected a property name enclosed in double quotes.`)
}
const key = await parseString()
await skipWhitespace()
if (lastChar !== ':') {
throw new SyntaxError(`Unexpected character '${lastChar}' at position ${pos}. Expected ':'.`)
}
await next()
const value = await parseValue()
expectValue(value)
obj[key] = value
await skipWhitespace()
if (lastChar === '}') {
await next()
return obj
} else if (lastChar === ',') {
await next()
await skipWhitespace()
} else {
throw new SyntaxError(`Unexpected character '${lastChar}' at position ${pos}. Expected ',' or '}'.`)
}
}
}
async function parseArray() {
if (lastChar !== '[') return
await next()
await skipWhitespace()
const array = []
if (lastChar === ']') {
await next()
return array
}
while (true) {
const value = await parseValue()
expectValue(value)
array.push(value)
await skipWhitespace()
if (lastChar === ']') {
await next()
return array
} else if (lastChar === ',') {
await next()
await skipWhitespace()
} else {
throw new SyntaxError(`Unexpected character '${lastChar}' at position ${pos}. Expected ',' or ']'.`)
}
}
}
async function parseKeyword(name, value) {
if (lastChar !== name[0]) return
for (let i = 1; i < name.length; i++) {
await next()
if (lastChar !== name[i]) {
throw new SyntaxError(`Unexpected character '${lastChar}' at position ${pos}.`)
}
}
await next()
if (isWhitespace(lastChar) || lastChar === ',' || lastChar === '}' || lastChar === ']' || lastChar === undefined) {
return value
}
throw new SyntaxError(`Unexpected character '${lastChar}' at position ${pos}.`)
}
async function skipWhitespace() {
while (isWhitespace(lastChar)) {
await next()
}
}
function isWhitespace(ch) {
return ch === ' ' || ch === '\n' || ch === '\t' || ch === '\r'
}
function isHexDigit(ch) {
return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')
}
function isDigit(ch) {
return ch >= '0' && ch <= '9'
}
function isInteger(value) {
return /^-?[0-9]+$/.test(value)
}
function toSafeNumber(str) {
const maxSafeInteger = Number.MAX_SAFE_INTEGER
const minSafeInteger = Number.MIN_SAFE_INTEGER
const num = BigInt(str)
return num >= minSafeInteger && num <= maxSafeInteger ? Number(num) : num
}
function expectValue(value) {
if (value === undefined) {
throw new SyntaxError(`JSON value expected but got '${value}' at position ${pos}. ${printErrorSnippet()}`)
}
}
}
function stringify(value, isPretty = false) {
const colors = {
string: isPretty ? '\x1b[32m' : '',
number: isPretty ? '\x1b[33m' : '',
boolean: isPretty ? '\x1b[35m' : '',
null: isPretty ? '\x1b[36m' : '',
reset: isPretty ? '\x1b[0m' : '',
key: isPretty ? '\x1b[1m' : '',
brace: isPretty ? '\x1b[1m' : '',
}
const indent = 2
function getIndent(level) {
return ' '.repeat(indent * level)
}
function stringifyValue(value, level = 0) {
if (typeof value === 'string') {
return `${colors.string}"${value}"${colors.reset}`
} else if (typeof value === 'number') {
return `${colors.number}${value}${colors.reset}`
} else if (typeof value === 'bigint') {
return `${colors.number}${value}${colors.reset}`
} else if (typeof value === 'boolean') {
return `${colors.boolean}${value}${colors.reset}`
} else if (value === null) {
return `${colors.null}null${colors.reset}`
} else if (Array.isArray(value)) {
if (value.length === 0) {
return `${colors.brace}[]${colors.reset}`
}
const items = value
.map((v) => `${getIndent(level + 1)}${stringifyValue(v, level + 1)}`)
.join(',\n')
return `${colors.brace}[\n${items}\n${getIndent(level)}]${colors.reset}`
} else if (typeof value === 'object') {
const keys = Object.keys(value)
if (keys.length === 0) {
return `${colors.brace}{${colors.reset}}`
}
const entries = keys
.map(
(key) =>
`${getIndent(level + 1)}${colors.key}"${key}"${colors.reset}: ${stringifyValue(
value[key],
level + 1
)}`
)
.join(',\n')
return `${colors.brace}{\n${entries}\n${getIndent(level)}}${colors.reset}`
}
throw new Error(`Unsupported value type: ${typeof value}`)
}
return stringifyValue(value)
}
function printUsage() {
const usage = `Usage
fx [flags] [code...]

@ -10,7 +10,7 @@ async function test(name, fn) {
async function run(json, code = '') {
const {spawnSync} = await import('node:child_process')
return spawnSync(`echo '${JSON.stringify(json)}' | node index.js ${code}`, {
return spawnSync(`echo '${typeof json === 'string' ? json : JSON.stringify(json)}' | node index.js ${code}`, {
stdio: 'pipe',
encoding: 'utf8',
shell: true
@ -18,72 +18,109 @@ async function run(json, code = '') {
}
void async function main() {
await test('as is', async t => {
await test('properly formatted', async t => {
const {stdout} = await run([{'greeting': 'hello world'}])
t.deepEqual(JSON.parse(stdout), [{'greeting': 'hello world'}])
t.deepEqual(stdout, '[\n {\n "greeting": "hello world"\n }\n]\n')
})
await test('anonymous function', async t => {
await test('parseJson - valid json', async t => {
const obj = {a: 2.3e100, b: 'str', c: null, d: false, e: [1, 2, 3]}
const {stdout, stderr} = await run(obj)
t.equal(stderr, '')
t.equal(stdout, JSON.stringify(obj, null, 2) + '\n')
})
await test('parseJson - invalid json', async t => {
const {stderr, status} = await run('{invalid}')
t.equal(status, 1)
t.ok(stderr.includes('SyntaxError'))
})
await test('parseJson - invalid number', async t => {
const {stderr, status} = await run('{"num": 12.3.4}')
t.equal(status, 1)
t.ok(stderr.includes('SyntaxError'))
})
await test('parseJson - numbers', async t => {
t.equal((await run('1.2e300')).stdout, '1.2e+300\n')
t.equal((await run('123456789012345678901234567890')).stdout, '123456789012345678901234567890\n')
t.equal((await run('23')).stdout, '23\n')
t.equal((await run('0')).stdout, '0\n')
t.equal((await run('0e+2')).stdout, '0\n')
t.equal((await run('0e+2')).stdout, '0\n')
t.equal((await run('0.0')).stdout, '0\n')
t.equal((await run('-0')).stdout, '0\n')
t.equal((await run('2.3')).stdout, '2.3\n')
t.equal((await run('2300e3')).stdout, '2300000\n')
t.equal((await run('2300e+3')).stdout, '2300000\n')
t.equal((await run('-2')).stdout, '-2\n')
t.equal((await run('2e-3')).stdout, '0.002\n')
t.equal((await run('2.3e-3')).stdout, '0.0023\n')
})
await test('transform - anonymous function', async t => {
const {stdout} = await run({'key': 'value'}, '\'function (x) { return x.key }\'')
t.equal(stdout, 'value\n')
})
await test('arrow function', async t => {
await test('transform - arrow function', async t => {
const {stdout} = await run({'key': 'value'}, '\'x => x.key\'')
t.equal(stdout, 'value\n')
})
await test('arrow function with param brackets', async t => {
await test('transform - arrow function with param brackets', async t => {
const {stdout} = await run({'key': 'value'}, `'(x) => x.key'`)
t.equal(stdout, 'value\n')
})
await test('this is json', async t => {
await test('transform - this is json', async t => {
const {stdout} = await run([1, 2, 3, 4, 5], `'this.map(x => x * this.length)'`)
t.deepEqual(JSON.parse(stdout), [5, 10, 15, 20, 25])
})
await test('chain works', async t => {
await test('transform - chain works', async t => {
const {stdout} = await run({'items': ['foo', 'bar']}, `'this.items' '.' 'x => x[1]'`)
t.equal(stdout, 'bar\n')
})
await test('map works', async t => {
await test('transform - map works', async t => {
const {stdout} = await run([1, 2, 3], `'map(x * 2)'`)
t.deepEqual(JSON.parse(stdout), [2, 4, 6])
})
await test('map works with dot', async t => {
await test('transform - map works with dot', async t => {
const {stdout} = await run([{foo: 'bar'}], `'map(.foo)'`)
t.deepEqual(JSON.parse(stdout), ['bar'])
})
await test('map works with func', async t => {
await test('transform - map works with func', async t => {
const {stdout} = await run([{foo: 'bar'}], `'map(x => x.foo)'`)
t.deepEqual(JSON.parse(stdout), ['bar'])
})
await test('map passes index', async t => {
await test('transform - map passes index', async t => {
const {stdout} = await run([1, 2, 3], `'map((x, i) => x * i)'`)
t.deepEqual(JSON.parse(stdout), [0, 2, 6])
})
await test('flat map works', async t => {
await test('transform - flat map works', async t => {
const {stdout} = await run({master: {foo: [{bar: [{val: 1}]}]}}, '.master.foo[].bar[].val')
t.deepEqual(JSON.parse(stdout), [1])
})
await test('flat map works on the first level', async t => {
await test('transform - flat map works on the first level', async t => {
const {stdout} = await run([{val: 1}, {val: 2}], '.[].val')
t.deepEqual(JSON.parse(stdout), [1, 2])
})
await test('sort & uniq', async t => {
await test('transform - sort & uniq', async t => {
const {stdout} = await run([2, 2, 3, 1], `sort uniq`)
t.deepEqual(JSON.parse(stdout), [1, 2, 3])
})
await test('invalid code argument', async t => {
await test('transform - invalid code argument', async t => {
const json = {foo: 'bar'}
const code = '".foo.toUpperCase("'
const {stderr, status} = await run(json, code)
@ -91,8 +128,13 @@ void async function main() {
t.ok(stderr.includes(`SyntaxError: Unexpected token '}'`))
})
await test('raw flag', async t => {
const {stdout} = await run(123, `-r 'x => typeof x'`)
t.equal(stdout, 'string\n')
await test('stream - json objects', async t => {
const {stdout} = await run('{"foo": "bar"}\n{"foo": "baz"}')
t.equal(stdout, '{\n "foo": "bar"\n}\n{\n "foo": "baz"\n}\n')
})
// await test('raw flag', async t => {
// const {stdout} = await run(123, `-r 'x => typeof x'`)
// t.equal(stdout, 'string\n')
// })
}()

Loading…
Cancel
Save