@sanity/mutator
Version:
A set of models to make it easier to utilize the powerful real time collaborative features of Sanity
291 lines (234 loc) • 6.97 kB
text/typescript
// A utility wrapper class to process parsed jsonpath expressions
import {descend} from './descend'
import {parseJsonPath} from './parse'
import {type Probe} from './Probe'
import {toPath} from './toPath'
import {type Expr, type HeadTail} from './types'
export interface Range {
start: number
end: number
step: number
}
export class Expression {
expr: Expr
constructor(expr: Expr | Expression | null) {
if (!expr) {
throw new Error('Attempted to create Expression from null-value')
}
// This is a wrapped expr
if ('expr' in expr) {
this.expr = expr.expr
} else {
this.expr = expr
}
if (!('type' in this.expr)) {
throw new Error('Attempt to create Expression for expression with no type')
}
}
isPath(): boolean {
return this.expr.type === 'path'
}
isUnion(): boolean {
return this.expr.type === 'union'
}
isCollection(): boolean {
return this.isPath() || this.isUnion()
}
isConstraint(): boolean {
return this.expr.type === 'constraint'
}
isRecursive(): boolean {
return this.expr.type === 'recursive'
}
isExistenceConstraint(): boolean {
return this.expr.type === 'constraint' && this.expr.operator === '?'
}
isIndex(): boolean {
return this.expr.type === 'index'
}
isRange(): boolean {
return this.expr.type === 'range'
}
expandRange(probe?: Probe): Range {
const probeLength = () => {
if (!probe) {
throw new Error('expandRange() required a probe that was not passed')
}
return probe.length()
}
let start = 'start' in this.expr ? this.expr.start || 0 : 0
start = interpretNegativeIndex(start, probe)
let end = 'end' in this.expr ? this.expr.end || probeLength() : probeLength()
end = interpretNegativeIndex(end, probe)
const step = 'step' in this.expr ? this.expr.step || 1 : 1
return {start, end, step}
}
isAttributeReference(): boolean {
return this.expr.type === 'attribute'
}
// Is a range or index -> something referencing indexes
isIndexReference(): boolean {
return this.isIndex() || this.isRange()
}
name(): string {
return 'name' in this.expr ? this.expr.name : ''
}
isSelfReference(): boolean {
return this.expr.type === 'alias' && this.expr.target === 'self'
}
constraintTargetIsSelf(): boolean {
return (
this.expr.type === 'constraint' &&
this.expr.lhs.type === 'alias' &&
this.expr.lhs.target === 'self'
)
}
constraintTargetIsAttribute(): boolean {
return this.expr.type === 'constraint' && this.expr.lhs.type === 'attribute'
}
testConstraint(probe: Probe): boolean {
const expr = this.expr
if (expr.type === 'constraint' && expr.lhs.type === 'alias' && expr.lhs.target === 'self') {
if (probe.containerType() !== 'primitive') {
return false
}
if (expr.type === 'constraint' && expr.operator === '?') {
return true
}
const lhs = probe.get()
const rhs = expr.rhs && 'value' in expr.rhs ? expr.rhs.value : undefined
return testBinaryOperator(lhs, expr.operator, rhs)
}
if (expr.type !== 'constraint') {
return false
}
const lhs = expr.lhs
if (!lhs) {
throw new Error('No LHS of expression')
}
if (lhs.type !== 'attribute') {
throw new Error(`Constraint target ${lhs.type} not supported`)
}
if (probe.containerType() !== 'object') {
return false
}
const lhsValue = probe.getAttribute(lhs.name)
if (lhsValue === undefined || lhsValue === null || lhsValue.containerType() !== 'primitive') {
// LHS is void and empty, or it is a collection
return false
}
if (this.isExistenceConstraint()) {
// There is no rhs, and if we're here the key did exist
return true
}
const rhs = expr.rhs && 'value' in expr.rhs ? expr.rhs.value : undefined
return testBinaryOperator(lhsValue.get(), expr.operator, rhs)
}
pathNodes(): Expr[] {
return this.expr.type === 'path' ? this.expr.nodes : [this.expr]
}
prepend(node: Expression): Expression {
if (!node) {
return this
}
return new Expression({
type: 'path',
nodes: node.pathNodes().concat(this.pathNodes()),
})
}
concat(other: Expression | null): Expression {
return other ? other.prepend(this) : this
}
descend(): HeadTail[] {
return descend(this.expr).map((headTail) => {
const [head, tail] = headTail
return {
head: head ? new Expression(head) : null,
tail: tail ? new Expression(tail) : null,
}
})
}
unwrapRecursive(): Expression {
if (this.expr.type !== 'recursive') {
throw new Error(`Attempt to unwrap recursive on type ${this.expr.type}`)
}
return new Expression(this.expr.term)
}
toIndicies(probe?: Probe): number[] {
if (this.expr.type !== 'index' && this.expr.type !== 'range') {
throw new Error('Node cannot be converted to indexes')
}
if (this.expr.type === 'index') {
return [interpretNegativeIndex(this.expr.value, probe)]
}
const result: number[] = []
const range = this.expandRange(probe)
let {start, end} = range
if (range.step < 0) {
;[start, end] = [end, start]
}
for (let i = start; i < end; i++) {
result.push(i)
}
return result
}
toFieldReferences(): number[] | string[] {
if (this.isIndexReference()) {
return this.toIndicies()
}
if (this.expr.type === 'attribute') {
return [this.expr.name]
}
throw new Error(`Can't convert ${this.expr.type} to field references`)
}
toString(): string {
return toPath(this.expr)
}
static fromPath(path: string): Expression {
const parsed = parseJsonPath(path)
if (!parsed) {
throw new Error(`Failed to parse path "${path}"`)
}
return new Expression(parsed)
}
static attributeReference(name: string): Expression {
return new Expression({
type: 'attribute',
name: name,
})
}
static indexReference(i: number): Expression {
return new Expression({
type: 'index',
value: i,
})
}
}
// Tests an operator on two given primitive values
function testBinaryOperator(lhsValue: any, operator: string, rhsValue: any) {
switch (operator) {
case '>':
return lhsValue > rhsValue
case '>=':
return lhsValue >= rhsValue
case '<':
return lhsValue < rhsValue
case '<=':
return lhsValue <= rhsValue
case '==':
return lhsValue === rhsValue
case '!=':
return lhsValue !== rhsValue
default:
throw new Error(`Unsupported binary operator ${operator}`)
}
}
function interpretNegativeIndex(index: number, probe?: Probe): number {
if (index >= 0) {
return index
}
if (!probe) {
throw new Error('interpretNegativeIndex() must have a probe when < 0')
}
return index + probe.length()
}