@sanity/mutator
Version:
A set of models to make it easier to utilize the powerful real time collaborative features of Sanity
187 lines (162 loc) • 5.09 kB
text/typescript
import {Descender} from './Descender'
import {Expression} from './Expression'
import {parseJsonPath} from './parse'
import {type Probe} from './Probe'
interface Result<P = unknown> {
leads: {
target: Expression
matcher: Matcher
}[]
delivery?: {
targets: Expression[]
payload: P
}
}
/**
* @internal
*/
export class Matcher {
active: Descender[]
recursives: Descender[]
payload: unknown
constructor(active: Descender[], parent?: Matcher) {
this.active = active || []
if (parent) {
this.recursives = parent.recursives
this.payload = parent.payload
} else {
this.recursives = []
}
this.extractRecursives()
}
setPayload(payload: unknown): this {
this.payload = payload
return this
}
// Moves any recursive descenders onto the recursive track, removing them from
// the active set
extractRecursives(): void {
this.active = this.active.filter((descender) => {
if (descender.isRecursive()) {
this.recursives.push(...descender.extractRecursives())
return false
}
return true
})
}
// Find recursives that are relevant now and should be considered part of the active set
activeRecursives(probe: Probe): Descender[] {
return this.recursives.filter((descender) => {
const head = descender.head
if (!head) {
return false
}
// Constraints are always relevant
if (head.isConstraint()) {
return true
}
// Index references are only relevant for indexable values
if (probe.containerType() === 'array' && head.isIndexReference()) {
return true
}
// Attribute references are relevant for plain objects
if (probe.containerType() === 'object') {
return head.isAttributeReference() && probe.hasAttribute(head.name())
}
return false
})
}
match(probe: Probe): Result {
return this.iterate(probe).extractMatches(probe)
}
iterate(probe: Probe): Matcher {
const newActiveSet: Descender[] = []
this.active.concat(this.activeRecursives(probe)).forEach((descender) => {
newActiveSet.push(...descender.iterate(probe))
})
return new Matcher(newActiveSet, this)
}
// Returns true if any of the descenders in the active or recursive set
// consider the current state a final destination
isDestination(): boolean {
return this.active.some((descender) => descender.hasArrived())
}
hasRecursives(): boolean {
return this.recursives.length > 0
}
// Returns any payload delivieries and leads that needs to be followed to complete
// the process.
extractMatches(probe: Probe): Result {
const leads: {target: Expression; matcher: Matcher}[] = []
const targets: Expression[] = []
this.active.forEach((descender) => {
if (descender.hasArrived()) {
// This was already arrived, so matches this value, not descenders
targets.push(
new Expression({
type: 'alias',
target: 'self',
}),
)
return
}
const descenderHead = descender.head
if (!descenderHead) {
return
}
if (probe.containerType() === 'array' && !descenderHead.isIndexReference()) {
// This descender does not match an indexable value
return
}
if (probe.containerType() === 'object' && !descenderHead.isAttributeReference()) {
// This descender never match a plain object
return
}
if (descender.tail) {
// Not arrived yet
const matcher = new Matcher(descender.descend(), this)
descenderHead.toFieldReferences().forEach(() => {
leads.push({
target: descenderHead,
matcher: matcher,
})
})
} else {
// arrived
targets.push(descenderHead)
}
})
// If there are recursive terms, we need to add a lead for every descendant ...
if (this.hasRecursives()) {
// The recustives matcher will have no active set, only inherit recursives from this
const recursivesMatcher = new Matcher([], this)
if (probe.containerType() === 'array') {
const length = probe.length()
for (let i = 0; i < length; i++) {
leads.push({
target: Expression.indexReference(i),
matcher: recursivesMatcher,
})
}
} else if (probe.containerType() === 'object') {
probe.attributeKeys().forEach((name) => {
leads.push({
target: Expression.attributeReference(name),
matcher: recursivesMatcher,
})
})
}
}
return targets.length > 0
? {leads: leads, delivery: {targets, payload: this.payload}}
: {leads: leads}
}
static fromPath(jsonpath: string): Matcher {
const path = parseJsonPath(jsonpath)
if (!path) {
throw new Error(`Failed to parse path from "${jsonpath}"`)
}
const descender = new Descender(null, new Expression(path))
return new Matcher(descender.descend())
}
}