@arkiv-network/sdk
Version:
TypeScript client library for Arkiv's blockchains interactions
112 lines (106 loc) • 3.53 kB
text/typescript
import { InvalidAttributeNameError } from "./errors"
/** The longest attribute name, in bytes — the width of the on-chain `Ident32` name cell. */
export const MAX_NAME_BYTES = 32
/**
* The attribute-name grammar (`Ident32`): a leading letter, then letters, digits, `.`, `-` or `_`.
*
* Names are **case-sensitive** — `Level` and `level` are two different attributes.
*
* The charset is ASCII-only, so a name's length in characters is also its length in bytes.
*/
const NAME_RE = /^[A-Za-z][A-Za-z0-9._-]*$/
/**
* Words the query language reserves. They are rejected as attribute names because an attribute
* named `not` or `str` could be written but never queried — the parser would read the name as an
* operator or a type tag.
*
* Matched **case-insensitively**: query keywords are case-insensitive, so `NOT` and `not` are the
* same operator, and the type tags are held to the same rule so that what is reserved does not
* depend on how you happen to capitalise it.
*/
const RESERVED = new Set([
// operators and keywords
"and",
"or",
"not",
"true",
"false",
"between",
"startswith",
"exists",
"typeof",
"order",
"by",
"asc",
"desc",
// type tags
"bool",
"i32",
"u64",
"u256",
"dec",
"bytes",
"str",
"addr",
"key",
])
/**
* Validates an attribute name against the engine's `Ident32` grammar and the query language's
* reserved words.
*
* @param name - The attribute name.
* @throws {InvalidAttributeNameError} If the name is empty, starts with `$` (reserved for system
* attributes), too long, uses characters outside the grammar, contains `--` (which opens a query
* comment), or collides with a reserved word.
*/
export function validateAttributeName(name: string): void {
if (typeof name !== "string" || name.length === 0) {
throw new InvalidAttributeNameError(String(name), "the name is empty")
}
if (name.startsWith("$")) {
throw new InvalidAttributeNameError(
name,
'"$" is reserved for system attributes, which the engine sets and you cannot write',
)
}
if (!NAME_RE.test(name)) {
const chars = [...name]
const at = chars.findIndex(
(char, index) => !(index === 0 ? /[A-Za-z]/ : /[A-Za-z0-9._-]/).test(char),
)
throw new InvalidAttributeNameError(
name,
at === 0
? `it starts with "${chars[0]}" rather than a letter`
: `it contains "${chars[at]}" at position ${at}`,
)
}
if (name.length > MAX_NAME_BYTES) {
throw new InvalidAttributeNameError(name, `the name is ${name.length} bytes long`)
}
// `-` is a legal name character and `--` opens a comment that runs to the end of the line, so a
// name containing one renders a query whose whole remainder is discarded
if (name.includes("--")) {
throw new InvalidAttributeNameError(
name,
'"--" opens a comment in the query language, so everything after it in a query would be ' +
"discarded — an attribute with this name could be written but never safely queried",
)
}
if (RESERVED.has(name.toLowerCase())) {
throw new InvalidAttributeNameError(
name,
`"${name}" is reserved by the query language, so an attribute with this name could never ` +
"be queried",
)
}
}
/** Whether `name` is a valid attribute name — the non-throwing form of {@link validateAttributeName}. */
export function isValidAttributeName(name: string): boolean {
try {
validateAttributeName(name)
return true
} catch {
return false
}
}