Skip to content

Commit

Permalink
fix: JSON parsers
Browse files Browse the repository at this point in the history
  • Loading branch information
epoberezkin committed Mar 7, 2021
1 parent 6cd8eb8 commit 8e07f30
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 54 deletions.
40 changes: 6 additions & 34 deletions lib/compile/jtd/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {SchemaEnv, getCompilingSchema} from ".."
import {_, str, and, nil, not, CodeGen, Code, Name, SafeExpr} from "../codegen"
import {MissingRefError} from "../error_classes"
import N from "../names"
import {isOwnProperty, hasPropFunc} from "../../vocabularies/code"
import {hasPropFunc} from "../../vocabularies/code"
import {hasRef} from "../../vocabularies/jtd/ref"
import {intRange, IntType} from "../../vocabularies/jtd/type"
import {parseJson, parseJsonNumber, parseJsonString} from "../../runtime/parseJson"
Expand Down Expand Up @@ -117,26 +117,6 @@ function parseCode(cxt: ParseCxt): void {

const parseBoolean = parseBooleanToken(true, parseBooleanToken(false, jsonSyntaxError))

// function parseEmptyCode(cxt: ParseCxt): void {
// const {gen, data, char: c} = cxt
// skipWhitespace(cxt)
// gen.assign(c, _`${N.json}[${N.jsonPos}]`)
// gen.if(_`${c} === "t" || ${c} === "f"`)
// parseBoolean(cxt)
// gen.elseIf(_`${c} === "n"`)
// tryParseToken(cxt, "null", jsonSyntaxError, () => gen.assign(data, null))
// gen.elseIf(_`${c} === '"'`)
// parseString(cxt)
// gen.elseIf(_`${c} === "["`)
// parseElements({...cxt, schema: {elements: {}}})
// gen.elseIf(_`${c} === "{"`)
// parseValues({...cxt, schema: {values: {}}})
// gen.else()
// parseNumber(cxt)
// gen.endIf()
// skipWhitespace(cxt)
// }

function parseNullable(cxt: ParseCxt, parseForm: GenParse): void {
const {gen, schema, data} = cxt
if (!schema.nullable) return parseForm(cxt)
Expand Down Expand Up @@ -171,15 +151,18 @@ function tryParseItems(cxt: ParseCxt, endToken: string, block: () => void): void
const {gen} = cxt
gen.for(_`;${N.jsonPos}<${N.jsonLen} && ${jsonSlice(1)}!==${endToken};`, () => {
block()
tryParseToken(cxt, ",", () => gen.break())
tryParseToken(cxt, ",", () => gen.break(), hasItem)
})

function hasItem(): void {
tryParseToken(cxt, endToken, () => {}, jsonSyntaxError)
}
}

function parseKeyValue(cxt: ParseCxt, schema: SchemaObject): void {
const {gen} = cxt
const key = gen.let("key")
parseString({...cxt, data: key})
checkDuplicateProperty(cxt, key)
parseToken(cxt, ":")
parsePropertyValue(cxt, key, schema)
}
Expand Down Expand Up @@ -231,11 +214,6 @@ function parseSchemaProperties(cxt: ParseCxt, discriminator?: string): void {
parseItems(cxt, "}", () => {
const key = gen.let("key")
parseString({...cxt, data: key})
if (discriminator) {
gen.if(_`${key} !== ${discriminator}`, () => checkDuplicateProperty(cxt, key))
} else {
checkDuplicateProperty(cxt, key)
}
parseToken(cxt, ":")
gen.if(false)
parseDefinedProperty(cxt, key, properties)
Expand Down Expand Up @@ -270,12 +248,6 @@ function parseDefinedProperty(cxt: ParseCxt, key: Name, schemas: SchemaObjectMap
}
}

function checkDuplicateProperty({gen, data}: ParseCxt, key: Name): void {
gen.if(isOwnProperty(gen, data, key), () =>
gen.throw(_`new Error("JSON: duplicate property " + ${key})`)
)
}

function parsePropertyValue(cxt: ParseCxt, key: Name, schema: SchemaObject): void {
parseCode({...cxt, schema, data: _`${cxt.data}[${key}]`})
}
Expand Down
30 changes: 19 additions & 11 deletions lib/runtime/parseJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ export function parseJson(s: string, pos: number): unknown {
return undefined
}
endPos = +matches[1]
const c = s[endPos]
s = s.slice(0, endPos)
parseJson.position = pos + endPos
try {
return JSON.parse(s)
} catch (e1) {
parseJson.message = `unexpected token ${s[endPos]}`
parseJson.message = `unexpected token ${c}`
return undefined
}
}
Expand Down Expand Up @@ -87,7 +88,8 @@ export function parseJsonNumber(s: string, pos: number, maxDigits?: number): num
}

function errorMessage(): void {
parseJson.message = pos < s.length ? `unexpected token ${s[pos]}` : "unexpected end"
parseJsonNumber.position = pos
parseJsonNumber.message = pos < s.length ? `unexpected token ${s[pos]}` : "unexpected end"
}
}

Expand All @@ -106,51 +108,57 @@ const escapedChars: {[X in string]?: string} = {
"\\": "\\",
}

const A_CODE: number = "a".charCodeAt(0)
const CODE_A: number = "a".charCodeAt(0)
const CODE_0: number = "0".charCodeAt(0)

export function parseJsonString(s: string, pos: number): string | undefined {
let str = ""
let c: string | undefined
parseJsonString.message = undefined
// eslint-disable-next-line no-constant-condition, @typescript-eslint/no-unnecessary-condition
while (true) {
c = s[pos]
pos++
c = s[pos++]
if (c === '"') break
if (c === "\\") {
c = s[pos]
if (c in escapedChars) {
str += escapedChars[c]
pos++
} else if (c === "u") {
pos++
let count = 4
let code = 0
while (count--) {
code <<= 4
c = s[pos].toLowerCase()
if (c >= "a" && c <= "f") {
c += c.charCodeAt(0) - A_CODE + 10
code += c.charCodeAt(0) - CODE_A + 10
} else if (c >= "0" && c <= "9") {
code += +c
code += c.charCodeAt(0) - CODE_0
} else if (c === undefined) {
errorMessage("unexpected end")
return undefined
} else {
errorMessage(`unexpected token ${s[pos]}`)
errorMessage(`unexpected token ${c}`)
return undefined
}
pos++
}
str += String.fromCharCode(code)
} else {
errorMessage(`unexpected token ${s[pos]}`)
errorMessage(`unexpected token ${c}`)
return undefined
}
pos++
} else if (c === undefined) {
errorMessage("unexpected end")
return undefined
} else {
str += c
if (c.charCodeAt(0) >= 0x20) {
str += c
} else {
errorMessage(`unexpected token ${c}`)
return undefined
}
}
}
parseJsonString.position = pos
Expand Down
4 changes: 2 additions & 2 deletions spec/json_parse_tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -1022,8 +1022,8 @@
{
"name": "string space",
"valid": true,
"json": "\" \"",
"data": " "
"json": "[\" \"]",
"data": [" "]
},
{
"name": "string start escape unclosed",
Expand Down
26 changes: 19 additions & 7 deletions spec/jtd-schema.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import getAjvInstances from "./ajv_instances"
import {withStandalone} from "./ajv_standalone"
import jtdValidationTests = require("./json-typedef-spec/tests/validation.json")
import jtdInvalidSchemasTests = require("./json-typedef-spec/tests/invalid_schemas.json")
// tests from https://github.com/nst/JSONTestSuite
import jsonParseTests = require("./json_parse_tests.json")
import assert = require("assert")
// import AjvPack from "../dist/standalone/instance"
Expand All @@ -25,6 +26,8 @@ interface JSONParseTest {
valid: boolean | null
json: string
data?: unknown
only?: boolean
skip?: boolean
}

interface JSONParseTestSuite {
Expand Down Expand Up @@ -154,26 +157,29 @@ describe("JSON Type Definition", () => {
const ajv = new _AjvJTD()
const parseJson: JTDParser = ajv.compileParser({})
const parse: {[K in "string" | "number" | "array" | "object"]: JTDParser} = {
string: ajv.compileParser({type: "string"}),
number: ajv.compileParser({type: "float64"}),
string: ajv.compileParser({elements: {type: "string"}}),
number: ajv.compileParser({elements: {type: "float64"}}),
array: ajv.compileParser({elements: {}}),
object: ajv.compileParser({values: {}}),
}

for (const {suite, tests} of jsonParseTests as JSONParseTestSuite[]) {
describe(suite, () => {
for (const {valid, name, json, data} of tests) {
for (const test of tests) {
const {valid, name, json, data} = test
if (valid) {
it(`should parse ${name}`, () => shouldParse(parseJson, json, data))
if (suite in parse) {
it.skip(`should parse as ${suite}: ${name}`, () =>
shouldParse(parse[suite], json, data))
_it(test)(`should parse as ${suite}: ${name}`, () =>
shouldParse(parse[suite], json, data)
)
}
} else if (valid === false) {
it(`should fail parsing ${name}`, () => shouldFail(parseJson, json))
if (suite in parse) {
it.skip(`should fail parsing as ${suite}: ${name}`, () =>
shouldFail(parse[suite], json))
_it(test)(`should fail parsing as ${suite}: ${name}`, () =>
shouldFail(parse[suite], json)
)
}
}
}
Expand All @@ -182,6 +188,12 @@ describe("JSON Type Definition", () => {
})
})

type TestFunc = typeof it | typeof it.only | typeof it.skip

function _it({only, skip}: JSONParseTest): TestFunc {
return skip ? it.skip : only ? it.only : it
}

function shouldParse(parse: JTDParser, str: string, res: unknown): void {
assert.deepStrictEqual(parse(str), res)
assert.strictEqual(parse.message, undefined)
Expand Down

0 comments on commit 8e07f30

Please sign in to comment.