Skip to content

Commit

Permalink
refactor: util.ts
Browse files Browse the repository at this point in the history
  • Loading branch information
epoberezkin committed Sep 23, 2020
1 parent 67117ba commit c31fec5
Show file tree
Hide file tree
Showing 31 changed files with 255 additions and 261 deletions.
10 changes: 0 additions & 10 deletions docs/keywords.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,16 +292,6 @@ Determines whether the passed schema has rules that should be validated. This fu
schemaHasRules(schema, it.RULES.all) // true or false
```

##### .getData(String jsonPointer, Number dataLevel, Array paths) -> String

Returns the validation-time expression to safely access data based on the passed [relative json pointer](https://tools.ietf.org/html/draft-luff-relative-json-pointer-00) (See [examples](https://gist.github.com/geraintluff/5911303)).

```javascript
getData("2/test/1", it.dataLevel, it.dataPathArr)
// The result depends on the current level
// if it.dataLevel is 3 the result is "data1 && data1.test && data1.test[1]"
```

##### .escapeJsonPointer(String str) -> String

Converts the property name to the JSON-Pointer fragment.
Expand Down
3 changes: 3 additions & 0 deletions lib/ajv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ export {
Schema,
SchemaObject,
AsyncSchema,
AnySchema,
ValidateFunction,
AsyncValidateFunction,
ErrorObject,
} from "./types"

export {SchemaCxt, SchemaObjCxt} from "./compile"
export interface Plugin<Opts> {
(ajv: Ajv, options?: Opts): Ajv
[prop: string]: any
Expand Down
47 changes: 43 additions & 4 deletions lib/compile/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ import type {
KeywordCxtParams,
AnySchemaObject,
} from "../types"
import {SchemaObjCxt} from "./index"
import {SchemaCxt, SchemaObjCxt} from "./index"
import {JSONType} from "./rules"
import {schemaRefOrVal} from "../vocabularies/util"
import {getData, checkDataTypes, DataType} from "./util"
import {checkDataTypes, DataType} from "./validate/dataType"
import {schemaRefOrVal, unescapeJsonPointer} from "./util"
import {
reportError,
reportExtraError,
resetErrorsCount,
keywordError,
keyword$DataError,
} from "./errors"
import {CodeGen, _, nil, or, Code, Name} from "./codegen"
import {CodeGen, _, nil, or, getProperty, Code, Name} from "./codegen"
import N from "./names"

export default class KeywordCxt implements KeywordErrorCxt {
Expand Down Expand Up @@ -202,3 +202,42 @@ function validateKeywordUsage(
}
}
}

const JSON_POINTER = /^\/(?:[^~]|~0|~1)*$/
const RELATIVE_JSON_POINTER = /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/
function getData($data: string, {dataLevel, dataNames, dataPathArr}: SchemaCxt): Code | number {
let jsonPointer
let data: Code
if ($data === "") return N.rootData
if ($data[0] === "/") {
if (!JSON_POINTER.test($data)) throw new Error(`Invalid JSON-pointer: ${$data}`)
jsonPointer = $data
data = N.rootData
} else {
const matches = RELATIVE_JSON_POINTER.exec($data)
if (!matches) throw new Error(`Invalid JSON-pointer: ${$data}`)
const up: number = +matches[1]
jsonPointer = matches[2]
if (jsonPointer === "#") {
if (up >= dataLevel) throw new Error(errorMsg("property/index", up))
return dataPathArr[dataLevel - up]
}
if (up > dataLevel) throw new Error(errorMsg("data", up))
data = dataNames[dataLevel - up]
if (!jsonPointer) return data
}

let expr = data
const segments = jsonPointer.split("/")
for (const segment of segments) {
if (segment) {
data = _`${data}${getProperty(unescapeJsonPointer(segment))}`
expr = _`${expr} && ${data}`
}
}
return expr

function errorMsg(pointerType: string, up: number): string {
return `Cannot access ${pointerType} ${up} levels up, current level is ${dataLevel}`
}
}
20 changes: 0 additions & 20 deletions lib/compile/ucs2length.ts

This file was deleted.

164 changes: 51 additions & 113 deletions lib/compile/util.ts
Original file line number Diff line number Diff line change
@@ -1,71 +1,8 @@
import type {AnySchema} from "../types"
import type {SchemaCxt} from "./index"
import type {JSONType, Rule, ValidationRules} from "./rules"
import {_, nil, and, operators, Code, Name, getProperty} from "./codegen"
import N from "./names"

export enum DataType {
Correct,
Wrong,
}

export function checkDataType(
dataType: JSONType,
data: Name,
strictNums?: boolean | "log",
correct = DataType.Correct
): Code {
const EQ = correct === DataType.Correct ? operators.EQ : operators.NEQ
let cond: Code
switch (dataType) {
case "null":
return _`${data} ${EQ} null`
case "array":
cond = _`Array.isArray(${data})`
break
case "object":
cond = _`${data} && typeof ${data} == "object" && !Array.isArray(${data})`
break
case "integer":
cond = numCond(_`!(${data} % 1) && !isNaN(${data})`)
break
case "number":
cond = numCond()
break
default:
return _`typeof ${data} ${EQ} ${dataType}`
}
return correct === DataType.Correct ? cond : _`!(${cond})`

function numCond(_cond: Code = nil): Code {
return and(_`typeof ${data} == "number"`, _cond, strictNums ? _`isFinite(${data})` : nil)
}
}

export function checkDataTypes(
dataTypes: JSONType[],
data: Name,
strictNums?: boolean | "log",
correct?: DataType
): Code {
if (dataTypes.length === 1) {
return checkDataType(dataTypes[0], data, strictNums, correct)
}
let cond: Code
const types = toHash(dataTypes)
if (types.array && types.object) {
const notObj = _`typeof ${data} != "object"`
cond = types.null ? notObj : _`(!${data} || ${notObj})`
delete types.null
delete types.array
delete types.object
} else {
cond = nil
}
if (types.number) delete types.integer
for (const t in types) cond = and(cond, checkDataType(t as JSONType, data, strictNums, correct))
return cond
}
import type {SchemaCxt, SchemaObjCxt} from "."
import {_, getProperty, Code} from "./codegen"
import type {Rule, ValidationRules} from "./rules"
import {checkStrictMode} from "./validate"

// TODO refactor to use Set
export function toHash<T extends string = string>(arr: T[]): {[K in T]?: true} {
Expand All @@ -74,6 +11,23 @@ export function toHash<T extends string = string>(arr: T[]): {[K in T]?: true} {
return hash
}

export function alwaysValidSchema(it: SchemaCxt, schema: AnySchema): boolean | void {
if (typeof schema == "boolean") return schema
if (Object.keys(schema).length === 0) return true
checkUnknownRules(it, schema)
return !schemaHasRules(schema, it.self.RULES.all)
}

export function checkUnknownRules(it: SchemaCxt, schema: AnySchema = it.schema): void {
const {opts, self} = it
if (!opts.strict) return
if (typeof schema === "boolean") return
const rules = self.RULES.keywords
for (const key in schema) {
if (!rules[key]) checkStrictMode(it, `unknown keyword: "${key}"`)
}
}

export function schemaHasRules(
schema: AnySchema,
rules: {[key: string]: boolean | Rule | undefined}
Expand All @@ -83,58 +37,23 @@ export function schemaHasRules(
return false
}

export function schemaCxtHasRules({schema, self}: SchemaCxt): boolean {
if (typeof schema == "boolean") return !schema
for (const key in schema) if (self.RULES.all[key]) return true
return false
}

export function schemaHasRulesButRef(schema: AnySchema, RULES: ValidationRules): boolean {
if (typeof schema == "boolean") return !schema
for (const key in schema) if (key !== "$ref" && RULES.all[key]) return true
return false
}

const JSON_POINTER = /^\/(?:[^~]|~0|~1)*$/
const RELATIVE_JSON_POINTER = /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/
export function getData(
$data: string,
{dataLevel, dataNames, dataPathArr}: SchemaCxt
): Code | number {
let jsonPointer
let data: Code
if ($data === "") return N.rootData
if ($data[0] === "/") {
if (!JSON_POINTER.test($data)) throw new Error(`Invalid JSON-pointer: ${$data}`)
jsonPointer = $data
data = N.rootData
} else {
const matches = RELATIVE_JSON_POINTER.exec($data)
if (!matches) throw new Error(`Invalid JSON-pointer: ${$data}`)
const up: number = +matches[1]
jsonPointer = matches[2]
if (jsonPointer === "#") {
if (up >= dataLevel) throw new Error(errorMsg("property/index", up))
return dataPathArr[dataLevel - up]
}
if (up > dataLevel) throw new Error(errorMsg("data", up))
data = dataNames[dataLevel - up]
if (!jsonPointer) return data
}

let expr = data
const segments = jsonPointer.split("/")
for (const segment of segments) {
if (segment) {
data = _`${data}${getProperty(unescapeJsonPointer(segment))}`
expr = _`${expr} && ${data}`
}
}
return expr

function errorMsg(pointerType: string, up: number): string {
return `Cannot access ${pointerType} ${up} levels up, current level is ${dataLevel}`
export function schemaRefOrVal(
{topSchemaRef, schemaPath}: SchemaObjCxt,
schema: unknown,
keyword: string,
$data?: string | false
): Code | number | boolean {
if (!$data) {
if (typeof schema == "number" || typeof schema == "boolean") return schema
if (typeof schema == "string") return _`${schema}`
}
return _`${topSchemaRef}${schemaPath}${getProperty(keyword)}`
}

export function unescapeFragment(str: string): string {
Expand All @@ -150,7 +69,7 @@ export function escapeJsonPointer(str: string | number): string {
return str.replace(/~/g, "~0").replace(/\//g, "~1")
}

function unescapeJsonPointer(str: string): string {
export function unescapeJsonPointer(str: string): string {
return str.replace(/~1/g, "/").replace(/~0/g, "~")
}

Expand All @@ -161,3 +80,22 @@ export function eachItem<T>(xs: T | T[], f: (x: T) => void): void {
f(xs)
}
}

// https://mathiasbynens.be/notes/javascript-encoding
// https://github.com/bestiejs/punycode.js - punycode.ucs2.decode
export function ucs2length(str: string): number {
const len = str.length
let length = 0
let pos = 0
let value: number
while (pos < len) {
length++
value = str.charCodeAt(pos++)
if (value >= 0xd800 && value <= 0xdbff && pos < len) {
// high surrogate, and there is a next character
value = str.charCodeAt(pos)
if ((value & 0xfc00) === 0xdc00) pos++ // low surrogate
}
}
return length
}
Loading

0 comments on commit c31fec5

Please sign in to comment.