docschema
Version:
Schema declaration and validation library using JsDoc comments
213 lines (180 loc) • 5.64 kB
JavaScript
import { findClosingBracketPosition } from '../functions/utils.js'
import { parsers } from './parsers.js'
/**
* @typedef {keyof parsers} ParserNames
*/
/**
* @type {string}
*/
const validParserNames = Object.keys(parsers)
.map((value) => `"${value}"`)
.join(', ')
/**
* @type {Record<ParserNames, string>}
*/
const validFilterNames = {
array: Object.keys(parsers.array).map((value) => `"${value}"`).join(', '),
number: Object.keys(parsers.number).map((value) => `"${value}"`).join(', '),
string: Object.keys(parsers.string).map((value) => `"${value}"`).join(', ')
}
/**
* @param {CurrentLocation} [currentLocation]
* @returns {string}
*/
function buildCurrentLocationString(currentLocation) {
return ((currentLocation)
? ` at ${currentLocation?.file}:${currentLocation?.startLine}`
: ''
)
}
/**
* @param {string} invalidKey
* @param {keyof parsers} validKey
* @param {CurrentLocation} [currentLocation]
* @throws {SyntaxError}
*/
function throwSyntaxErrorAboutInvalidFilter(
invalidKey, validKey, currentLocation
) {
throw new SyntaxError(
`"${invalidKey}" is not a valid filter for type "${validKey}"`
+ buildCurrentLocationString(currentLocation)
+ `. Use any of these filters: ${validFilterNames[validKey]}`
)
}
/**
* @param {ParserNames} typeName
* @param {Filters} filters
* @param {CurrentLocation} [currentLocation]
* @throws {SyntaxError}
*/
function validateParser(typeName, filters, currentLocation) {
if (!(typeName in parsers)) {
throw new SyntaxError(
/*
* Note: Do not enquote typeName, because it doesn't
* look good when the type is array or similar
*/
`Filters are not compatible with ${typeName} type`
+ buildCurrentLocationString(currentLocation)
+ `. Use filters only with types: ${validParserNames}`
)
}
for (const key in filters) {
if (!(key in parsers[typeName])) {
throwSyntaxErrorAboutInvalidFilter(key, typeName, currentLocation)
continue
}
const shouldBe = parsers[typeName][key]
let filterValue = filters[key]
if (shouldBe !== Array && filterValue instanceof Array) {
if (filterValue.length === 0) {
throw new SyntaxError(
`Filter "${key}" (used for ${typeName} type) is an empty Array`
+ buildCurrentLocationString(currentLocation)
+ '. When the filter is an Array, it should be a tuple with the'
+ ' filter value at index 0 and a custom error message at index 1.'
+ ` In ${JSON.stringify(filters)}`
)
}
filterValue = filterValue[0]
}
const typeActual = typeof filterValue
if (typeof shouldBe === 'string') {
const typeShouldBe = shouldBe
if (typeActual !== typeShouldBe) {
throw new SyntaxError(
`Filter "${key}" (used for ${typeName} type)`
+ ' has a value with wrong type'
+ buildCurrentLocationString(currentLocation)
+ `. The type for filter "${key}" should be "${typeShouldBe}",`
+ ` but it is "${typeActual}" instead, in ${JSON.stringify(filters)}`
)
}
}
else {
if (!(filterValue instanceof shouldBe)) {
throw new SyntaxError(
`Filter "${key}" (used for ${typeName} type)`
+ ' has a value with wrong type'
+ buildCurrentLocationString(currentLocation)
+ `. The type for filter "${key}" should be "${shouldBe.name}",`
+ ` but it is "${typeActual}" instead, in ${JSON.stringify(filters)}`
)
}
}
}
}
/**
* @param {string} description
* @returns {string}
*/
function flattenDescription(description) {
let flatten = description
// Remove the '-' and empty chars from the beginning
flatten = flatten.replace(/^ *-? */u, '')
// Remove all new rows
flatten = flatten
.replaceAll('\r', '')
.replaceAll('\n', ' ')
return flatten
}
/**
* @param {Filters} filters
* @returns {TupleFilters}
*/
function transformFiltersIntoTuples(filters) {
const tupleFilters = {}
for (const filterName in filters) {
tupleFilters[filterName] = (filters[filterName] instanceof Array)
? filters[filterName]
: [filters[filterName], '']
}
return tupleFilters
}
/**
* @param {string} inputDescription
* @param {ParsedType[]} types
* @param {CurrentLocation} [currentLocation]
* @returns {{
* description: string,
* filters: TupleFilters
* }}
* @throws {SyntaxError | Error}
*/
export function parse(inputDescription, types, currentLocation) {
const output = { description: '', filters: {} }
if (!inputDescription) {
return output
}
const description = flattenDescription(inputDescription)
const openBracketPos = description.indexOf('{')
// No filters found
if (openBracketPos === -1) {
output.description = description
}
// Filters found
else {
const closeBracketPos = findClosingBracketPosition(
description, openBracketPos
)
if (closeBracketPos > 0) {
const filtersStr = description.substring(
openBracketPos,
openBracketPos + closeBracketPos + 1
)
output.description = (
description.substring(0, openBracketPos).trim()
+ description.substring(openBracketPos + closeBracketPos + 1)
).trim()
const filters = Function(`return ${filtersStr}`)()
for (const type of types) {
// @ts-expect-error
validateParser(type.typeName, filters, currentLocation)
}
const filtersAsTuples = transformFiltersIntoTuples(filters)
output.filters = filtersAsTuples
}
}
return output
}