Skip to content

Commit

Permalink
Fix condition()
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky committed Jan 30, 2022
1 parent 43703a8 commit 9f684e7
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 39 deletions.
48 changes: 48 additions & 0 deletions src/config/normalize/lib/condition.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { groupBy } from '../../../utils/group.js'
import { mapValues } from '../../../utils/map.js'

import { callValueFunc } from './call.js'
import { remove } from './prop_path/set.js'

// Apply `condition(value, opts)` which skips the current definition if `false`
// is returned.
export const againstCondition = async function (value, condition, opts) {
return (
condition !== undefined && !(await callValueFunc(condition, value, opts))
)
}

// `condition(value, opts)` has two purposes:
// - Applying different definitions for a given property based on a condition
// - Skipping specific properties based on a condition
// - For example, when several commands share some properties but not all
// For the second purpose, we remove properties when all their `condition()`
// return `false`.
// We do this by:
// - Counting how many definitions each query has
// - Decrementing that count each time all `condition()` of a given property
// (including wildcard iteration) return `false`
// - Removing the property if that count is 0
export const getSkipCounts = function (definitions) {
return mapValues(groupBy(definitions, 'name'), getSkipCount)
}

const getSkipCount = function (definitions) {
return definitions.length
}

export const applySkipCounts = function ({
config,
allSkipped,
skipCounts,
query,
}) {
if (!allSkipped) {
return { config, skipCounts }
}

const skipCount = skipCounts[query] - 1
const skipCountsA = { ...skipCounts, [query]: skipCount }
const configA = skipCount === 0 ? remove(config, query) : config
return { config: configA, skipCounts: skipCountsA }
}
17 changes: 6 additions & 11 deletions src/config/normalize/lib/definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,24 @@
import { AssertionError } from 'assert'

import { callValueFunc, callUserFunc } from './call.js'
import { againstCondition } from './condition.js'

export const applyDefinition = async function (
{ condition, default: defaultValue, compute, validate, transform },
value,
opts,
) {
if (await againstCondition(value, condition, opts)) {
return value
const skipped = await againstCondition(value, condition, opts)

if (skipped) {
return { value, skipped }
}

const valueA = await addDefaultValue(value, defaultValue, opts)
const valueB = await computeValue(valueA, compute, opts)
await validateValue(valueB, validate, opts)
const valueC = await transformValue(valueB, transform, opts)
return valueC
}

// Apply `condition(opts)` which skips the current definition if `false` is
// returned.
const againstCondition = async function (value, condition, opts) {
return (
condition !== undefined && !(await callValueFunc(condition, value, opts))
)
return { value: valueC, skipped }
}

// Apply `default(opts)` which assigns a default value
Expand Down
63 changes: 36 additions & 27 deletions src/config/normalize/lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import pReduce from 'p-reduce'

import { cleanObject } from '../../../utils/clean.js'

import { getSkipCounts, applySkipCounts } from './condition.js'
import { applyDefinition } from './definitions.js'
import { list } from './prop_path/get.js'
import { parse } from './prop_path/parse.js'
Expand All @@ -23,46 +24,54 @@ export const normalizeConfigProps = async function (
definitions,
{ context, loose = false },
) {
const skipCounts = getSkipCounts(definitions)

try {
const configB = await pReduce(
const { config: configA } = await pReduce(
definitions,
(configA, definition) =>
applyDefinitionDeep(configA, definition, context),
config,
(memo, definition) => applyDefinitionDeep(memo, { definition, context }),
{ config, skipCounts },
)
const configC = cleanObject(configB)
return configC
const configB = cleanObject(configA)
return configB
} catch (error) {
return handleError(error, loose)
}
}

const applyDefinitionDeep = async function (config, definition, context) {
const props = Object.entries(list(config, definition.name))
return await pReduce(
const applyDefinitionDeep = async function (
{ config, skipCounts },
{ definition, definition: { name: query }, context },
) {
const props = Object.entries(list(config, query))
const { config: configA, allSkipped } = await pReduce(
props,
(configA, [name, value]) =>
applyPropDefinition({
value,
name,
definition,
config: configA,
context,
}),
config,
(memo, [name, value]) =>
applyPropDefinition(memo, { value, name, definition, context }),
{ config, allSkipped: true },
)
const { config: configB, skipCounts: skipCountsA } = applySkipCounts({
config: configA,
allSkipped,
skipCounts,
query,
})
return { config: configB, skipCounts: skipCountsA }
}

const applyPropDefinition = async function ({
value,
name,
definition,
config,
context,
}) {
const applyPropDefinition = async function (
{ config, allSkipped },
{ value, name, definition, context },
) {
const opts = getOpts(name, config, context)
const newValue = await applyDefinition(definition, value, opts)
return set(config, name, newValue)
const { value: newValue, skipped } = await applyDefinition(
definition,
value,
opts,
)
const configA = set(config, name, newValue)
const allSkippedA = allSkipped && skipped
return { config: configA, allSkipped: allSkippedA }
}

// Retrieve `opts` passed to most methods
Expand Down
4 changes: 4 additions & 0 deletions src/config/normalize/lib/prop_path/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
// TODO: allow special characters (like dots), if escaped with backslash
// TODO: do not recurse over `__proto__`, `prototype` or `constructor`
export const parse = function (query) {
if (query === '') {
return []
}

const normalizedQuery = prependDot(query)
const matchResults = [...normalizedQuery.matchAll(QUERY_REGEXP)]
validateQuery(matchResults, query, normalizedQuery)
Expand Down
55 changes: 54 additions & 1 deletion src/config/normalize/lib/prop_path/set.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import omit from 'omit.js'

import { setArray } from '../../../../utils/set.js'

import { listEntries } from './entries.js'
Expand All @@ -19,7 +21,58 @@ const setProp = function (value, index, { path, setValue }) {
}

const key = path[index]
const newChildValue = setProp(value[key], index + 1, { path, setValue })
const childValue = value[key]
const newIndex = index + 1
const newChildValue = setProp(childValue, newIndex, { path, setValue })
return setNewChildValue(value, key, newChildValue)
}

// Delete one or multiple properties in `target` using a query string
export const remove = function (target, query) {
const tokens = parse(query)
const entries = listEntries(target, tokens)
return entries.reduce(
(targetA, { path }) => removeProp(targetA, 0, path),
target,
)
}

const removeProp = function (value, index, path) {
const key = path[index]
const childValue = value[key]

if (childValue === undefined) {
return value
}

const newIndex = index + 1

if (newIndex === path.length) {
return removeValue(value, key)
}

const newChildValue = removeProp(childValue, newIndex, path)
return setNewChildValue(value, key, newChildValue)
}

const removeValue = function (value, key) {
if (typeof key === 'string') {
return omit.default(value, [key])
}

const newArray = setArray(value, key)
return newArray.every(isUndefined) ? [] : newArray
}

const isUndefined = function (item) {
return item === undefined
}

const setNewChildValue = function (value, key, newChildValue) {
if (value[key] === newChildValue) {
return value
}

return typeof key === 'string'
? { ...value, [key]: newChildValue }
: setArray(value, key, newChildValue)
Expand Down

0 comments on commit 9f684e7

Please sign in to comment.