import { SUM_OPS } from 'vega-lite/build/src/aggregate' ; import * as CHANNEL from 'vega-lite/build/src/channel' ; import { NONPOSITION_CHANNELS , supportMark } from 'vega-lite/build/src/channel' ; import * as MARK from 'vega-lite/build/src/mark' ; import { Mark } from 'vega-lite/build/src/mark' ; import { ScaleType } from 'vega-lite/build/src/scale' ; import * as TYPE from 'vega-lite/build/src/type' ; import { QueryConfig } from '../config' ; import { SpecQueryModel } from '../model' ; import { getEncodingNestedProp , isEncodingNestedProp , isEncodingProperty , Property } from '../property' ; import { PropIndex } from '../propindex' ; import { EncodingQuery , FieldQuery , isAutoCountQuery , isDimension , isDisabledAutoCountQuery , isEnabledAutoCountQuery , isFieldQuery , isMeasure , isValueQuery , ScaleQuery , scaleType , } from '../query/encoding' ; import { ExpandedType } from '../query/expandedtype' ; import { Schema } from '../schema' ; import { contains , every , some } from '../util' ; import { isWildcard , Wildcard } from '../wildcard' ; import { AbstractConstraint , AbstractConstraintModel } from './base' ; const NONPOSITION_CHANNELS_INDEX = NONPOSITION_CHANNELS . reduce ( ( m , channel ) => { m [ channel ] = true ; return m ; } , { } ) ; export interface SpecConstraintChecker { ( specM , schema , opt ) : boolean ; } export class SpecConstraintModel extends AbstractConstraintModel { constructor ( specConstraint ) { super ( specConstraint ) ; } public hasAllRequiredPropertiesSpecific ( specM ) { return every ( this . constraint . properties , ( prop ) => { if ( prop === Property . MARK ) { return ! isWildcard ( specM . getMark ( ) ) ; } if ( isEncodingNestedProp ( prop ) ) { const parent = prop . parent ; const child = prop . child ; return every ( specM . getEncodings ( ) , ( encQ ) => { if ( ! encQ [ parent ] ) { return true ; } return ! isWildcard ( encQ [ parent ] [ child ] ) ; } ) ; } if ( ! isEncodingProperty ( prop ) ) { throw new Error ( 'UNIMPLEMENTED' ) ; } return every ( specM . getEncodings ( ) , ( encQ ) => { if ( ! encQ [ prop ] ) { return true ; } return ! isWildcard ( encQ [ prop ] ) ; } ) ; } ) ; } public satisfy ( specM , schema , opt ) { if ( ! this . constraint . allowWildcardForProperties ) { if ( ! this . hasAllRequiredPropertiesSpecific ( specM ) ) { return true ; } } return ( this . constraint as SpecConstraint ) . satisfy ( specM , schema , opt ) ; } } export interface SpecConstraint extends AbstractConstraint { /** Method for checking if the spec query satisfies this constraint. */ satisfy : SpecConstraintChecker ; } export const SPEC_CONSTRAINTS = [ { name : 'noRepeatedChannel' , description : 'Each encoding channel should only be used once.' , properties : [ Property . CHANNEL ] , allowWildcardForProperties : true , strict : true , satisfy : ( specM , _ , __ ) => { const usedChannel = { } ; return every ( specM . getEncodings ( ) , ( encQ ) => { if ( ! isWildcard ( encQ . channel ) ) { if ( usedChannel [ encQ . channel ] ) { return false ; } usedChannel [ encQ . channel ] = true ; return true ; } return true ; } ) ; } , } , { name : 'alwaysIncludeZeroInScaleWithBarMark' , description : 'Do not recommend bar mark if scale does not start at zero' , properties : [ Property . MARK , Property . SCALE , getEncodingNestedProp ( 'scale' , 'zero' ) , Property . CHANNEL , Property . TYPE , ] , allowWildcardForProperties : false , strict : true , satisfy : ( specM , _ , __ ) => { const mark = specM . getMark ( ) ; const encodings = specM . getEncodings ( ) ; if ( mark === MARK . BAR ) { for ( const encQ of encodings ) { if ( isFieldQuery ( encQ ) && ( encQ . channel === CHANNEL . X || encQ . channel === CHANNEL . Y ) && encQ . type === TYPE . QUANTITATIVE && encQ . scale && ( encQ . scale as ScaleQuery ) . zero === false ) { return false ; } } } return true ; } , } , { name : 'autoAddCount' , description : 'Automatically adding count only for plots with only ordinal, binned quantitative, or temporal with timeunit fields.' , properties : [ Property . BIN , Property . TIMEUNIT , Property . TYPE , Property . AUTOCOUNT ] , allowWildcardForProperties : true , strict : false , satisfy : ( specM , _ , __ ) => { const hasAutoCount = some ( specM . getEncodings ( ) , ( encQ ) => isEnabledAutoCountQuery ( encQ ) ) ; if ( hasAutoCount ) { return every ( specM . getEncodings ( ) , ( encQ ) => { if ( isValueQuery ( encQ ) ) { return true ; } if ( isAutoCountQuery ( encQ ) ) { return true ; } switch ( encQ . type ) { case TYPE . QUANTITATIVE : return ! ! encQ . bin ; case TYPE . TEMPORAL : return ! ! encQ . timeUnit ; case TYPE . ORDINAL : case ExpandedType . KEY : case TYPE . NOMINAL : return true ; } throw new Error ( 'Unsupported Type' ) ; } ) ; } else { const autoCountEncIndex = specM . wildcardIndex . encodingIndicesByProperty . get ( 'autoCount' ) || [ ] ; const neverHaveAutoCount = every ( autoCountEncIndex , ( index ) => { const encQ = specM . getEncodingQueryByIndex ( index ) ; return isAutoCountQuery ( encQ ) && ! isWildcard ( encQ . autoCount ) ; } ) ; if ( neverHaveAutoCount ) { return some ( specM . getEncodings ( ) , ( encQ ) => { if ( ( isFieldQuery ( encQ ) || isAutoCountQuery ( encQ ) ) && encQ . type === TYPE . QUANTITATIVE ) { if ( isDisabledAutoCountQuery ( encQ ) ) { return false ; } else { return isFieldQuery ( encQ ) && ( ! encQ . bin || isWildcard ( encQ . bin ) ) ; } } else if ( isFieldQuery ( encQ ) && encQ . type === TYPE . TEMPORAL ) { return ! encQ . timeUnit || isWildcard ( encQ . timeUnit ) ; } return false ; } ) ; } } return true ; } , } , { name : 'channelPermittedByMarkType' , description : 'Each encoding channel should be supported by the mark type' , properties : [ Property . CHANNEL , Property . MARK ] , allowWildcardForProperties : true , strict : true , satisfy : ( specM , _ , __ ) => { const mark = specM . getMark ( ) ; if ( isWildcard ( mark ) ) return true ; return every ( specM . getEncodings ( ) , ( encQ ) => { if ( isWildcard ( encQ . channel ) ) return true ; if ( encQ . channel === 'row' || encQ . channel === 'column' || encQ . channel === 'facet' ) return true ; return ! ! supportMark ( encQ . channel , mark as Mark ) ; } ) ; } , } , { name : 'hasAllRequiredChannelsForMark' , description : 'All required channels for the specified mark should be specified' , properties : [ Property . CHANNEL , Property . MARK ] , allowWildcardForProperties : false , strict : true , satisfy : ( specM , _ , __ ) => { const mark = specM . getMark ( ) ; switch ( mark ) { case MARK . AREA : case MARK . LINE : return specM . channelUsed ( CHANNEL . X ) && specM . channelUsed ( CHANNEL . Y ) ; case MARK . TEXT : return specM . channelUsed ( CHANNEL . TEXT ) ; case MARK . BAR : case MARK . CIRCLE : case MARK . SQUARE : case MARK . TICK : case MARK . RULE : case MARK . RECT : return specM . channelUsed ( CHANNEL . X ) || specM . channelUsed ( CHANNEL . Y ) ; case MARK . POINT : return ( ! specM . wildcardIndex . hasProperty ( Property . CHANNEL ) || specM . channelUsed ( CHANNEL . X ) || specM . channelUsed ( CHANNEL . Y ) ) ; } throw new Error ( `hasAllRequiredChannelsForMark not implemented for mark${ JSON . stringify ( mark ) }` ) ; } , } , { name : 'omitAggregate' , description : 'Omit aggregate plots.' , properties : [ Property . AGGREGATE , Property . AUTOCOUNT ] , allowWildcardForProperties : true , strict : false , satisfy : ( specM , _ , __ ) => { if ( specM . isAggregate ( ) ) { return false ; } return true ; } , } , { name : 'omitAggregatePlotWithDimensionOnlyOnFacet' , description : 'Omit aggregate plots with dimensions only on facets as that leads to inefficient use of space.' , properties : [ Property . CHANNEL , Property . AGGREGATE , Property . AUTOCOUNT ] , allowWildcardForProperties : false , strict : false , satisfy : ( specM , _ , opt ) => { if ( specM . isAggregate ( ) ) { let hasNonFacetDim = false ; let hasDim = false ; let hasEnumeratedFacetDim = false ; specM . specQuery . encodings . forEach ( ( encQ , index ) => { if ( isValueQuery ( encQ ) || isDisabledAutoCountQuery ( encQ ) ) return ; if ( isFieldQuery ( encQ ) && ! encQ . aggregate ) { hasDim = true ; if ( contains ( [ CHANNEL . ROW , CHANNEL . COLUMN ] , encQ . channel ) ) { if ( specM . wildcardIndex . hasEncodingProperty ( index , Property . CHANNEL ) ) { hasEnumeratedFacetDim = true ; } } else { hasNonFacetDim = true ; } } } ) ; if ( hasDim && ! hasNonFacetDim ) { if ( hasEnumeratedFacetDim || opt . constraintManuallySpecifiedValue ) { return false ; } } } return true ; } , } , { name : 'omitAggregatePlotWithoutDimension' , description : 'Aggregate plots without dimension should be omitted' , properties : [ Property . AGGREGATE , Property . AUTOCOUNT , Property . BIN , Property . TIMEUNIT , Property . TYPE ] , allowWildcardForProperties : false , strict : false , satisfy : ( specM , _ , __ ) => { if ( specM . isAggregate ( ) ) { return some ( specM . getEncodings ( ) , ( encQ ) => { if ( isDimension ( encQ ) || ( isFieldQuery ( encQ ) && encQ . type === 'temporal' ) ) { return true ; } return false ; } ) ; } return true ; } , } , { name : 'omitBarLineAreaWithOcclusion' , description : "Don't use bar, line or area to visualize raw plot as they often lead to occlusion." , properties : [ Property . MARK , Property . AGGREGATE , Property . AUTOCOUNT ] , allowWildcardForProperties : false , strict : false , satisfy : ( specM , _ , __ ) => { if ( contains ( [ MARK . BAR , MARK . LINE , MARK . AREA ] , specM . getMark ( ) ) ) { return specM . isAggregate ( ) ; } return true ; } , } , { name : 'omitBarTickWithSize' , description : 'Do not map field to size channel with bar and tick mark' , properties : [ Property . CHANNEL , Property . MARK ] , allowWildcardForProperties : true , strict : false , satisfy : ( specM , _ , opt ) => { const mark = specM . getMark ( ) ; if ( contains ( [ MARK . TICK , MARK . BAR ] , mark ) ) { if ( specM . channelEncodingField ( CHANNEL . SIZE ) ) { if ( opt . constraintManuallySpecifiedValue ) { return false ; } else { const encodings = specM . specQuery . encodings ; for ( let i = 0 ; i < encodings . length ; i ++ ) { const encQ = encodings [ i ] ; if ( encQ . channel === CHANNEL . SIZE ) { if ( specM . wildcardIndex . hasEncodingProperty ( i , Property . CHANNEL ) ) { return false ; } else { return true ; } } } } } } return true ; } , } , { name : 'omitBarAreaForLogScale' , description : "Do not use bar and area mark for x and y's log scale" , properties : [ Property . MARK , Property . CHANNEL , Property . SCALE , getEncodingNestedProp ( 'scale' , 'type' ) , Property . TYPE , ] , allowWildcardForProperties : false , strict : true , satisfy : ( specM , _ , __ ) => { const mark = specM . getMark ( ) ; const encodings = specM . getEncodings ( ) ; if ( mark === MARK . AREA || mark === MARK . BAR ) { for ( const encQ of encodings ) { if ( isFieldQuery ( encQ ) && ( encQ . channel === CHANNEL . X || encQ . channel === CHANNEL . Y ) && encQ . scale ) { const sType = scaleType ( encQ ) ; if ( sType === ScaleType . LOG ) { return false ; } } } } return true ; } , } , { name : 'omitMultipleNonPositionalChannels' , description : 'Unless manually specified, do not use multiple non-positional encoding channel to avoid over-encoding.' , properties : [ Property . CHANNEL ] , allowWildcardForProperties : true , strict : false , satisfy : ( specM , _ , opt ) => { const encodings = specM . specQuery . encodings ; let nonPositionChannelCount = 0 ; let hasEnumeratedNonPositionChannel = false ; for ( let i = 0 ; i < encodings . length ; i ++ ) { const encQ = encodings [ i ] ; if ( isValueQuery ( encQ ) || isDisabledAutoCountQuery ( encQ ) ) { continue ; } const channel = encQ . channel ; if ( ! isWildcard ( channel ) ) { if ( NONPOSITION_CHANNELS_INDEX [ `${ channel }` ] ) { nonPositionChannelCount += 1 ; if ( specM . wildcardIndex . hasEncodingProperty ( i , Property . CHANNEL ) ) { hasEnumeratedNonPositionChannel = true ; } if ( nonPositionChannelCount > 1 && ( hasEnumeratedNonPositionChannel || opt . constraintManuallySpecifiedValue ) ) { return false ; } } } } return true ; } , } , { name : 'omitNonPositionalOrFacetOverPositionalChannels' , description : 'Do not use non-positional channels unless all positional channels are used' , properties : [ Property . CHANNEL ] , allowWildcardForProperties : false , strict : false , satisfy : ( specM , _ , opt ) => { const encodings = specM . specQuery . encodings ; let hasNonPositionalChannelOrFacet = false ; let hasEnumeratedNonPositionOrFacetChannel = false ; let hasX = false ; let hasY = false ; for ( let i = 0 ; i < encodings . length ; i ++ ) { const encQ = encodings [ i ] ; if ( isValueQuery ( encQ ) || isDisabledAutoCountQuery ( encQ ) ) { continue ; } const channel = encQ . channel ; if ( channel === CHANNEL . X ) { hasX = true ; } else if ( channel === CHANNEL . Y ) { hasY = true ; } else if ( ! isWildcard ( channel ) ) { hasNonPositionalChannelOrFacet = true ; if ( specM . wildcardIndex . hasEncodingProperty ( i , Property . CHANNEL ) ) { hasEnumeratedNonPositionOrFacetChannel = true ; } } } if ( hasEnumeratedNonPositionOrFacetChannel || ( opt . constraintManuallySpecifiedValue && hasNonPositionalChannelOrFacet ) ) { return hasX && hasY ; } return true ; } , } , { name : 'omitRaw' , description : 'Omit raw plots.' , properties : [ Property . AGGREGATE , Property . AUTOCOUNT ] , allowWildcardForProperties : false , strict : false , satisfy : ( specM , _ , __ ) => { if ( ! specM . isAggregate ( ) ) { return false ; } return true ; } , } , { name : 'omitRawContinuousFieldForAggregatePlot' , description : 'Aggregate plot should not use raw continuous field as group by values. ' + '(Quantitative should be binned. Temporal should have time unit.)' , properties : [ Property . AGGREGATE , Property . AUTOCOUNT , Property . TIMEUNIT , Property . BIN , Property . TYPE ] , allowWildcardForProperties : true , strict : false , satisfy : ( specM , _ , opt ) => { if ( specM . isAggregate ( ) ) { const encodings = specM . specQuery . encodings ; for ( let i = 0 ; i < encodings . length ; i ++ ) { const encQ = encodings [ i ] ; if ( isValueQuery ( encQ ) || isDisabledAutoCountQuery ( encQ ) ) continue ; if ( isFieldQuery ( encQ ) && encQ . type === TYPE . TEMPORAL ) { if ( ! encQ . timeUnit && ( specM . wildcardIndex . hasEncodingProperty ( i , Property . TIMEUNIT ) || opt . constraintManuallySpecifiedValue ) ) { return false ; } } if ( encQ . type === TYPE . QUANTITATIVE ) { if ( isFieldQuery ( encQ ) && ! encQ . bin && ! encQ . aggregate ) { if ( specM . wildcardIndex . hasEncodingProperty ( i , Property . BIN ) || specM . wildcardIndex . hasEncodingProperty ( i , Property . AGGREGATE ) || specM . wildcardIndex . hasEncodingProperty ( i , Property . AUTOCOUNT ) ) { return false ; } if ( opt . constraintManuallySpecifiedValue ) { return false ; } } } } } return true ; } , } , { name : 'omitRawDetail' , description : 'Do not use detail channel with raw plot.' , properties : [ Property . CHANNEL , Property . AGGREGATE , Property . AUTOCOUNT ] , allowWildcardForProperties : false , strict : true , satisfy : ( specM , _ , opt ) => { if ( specM . isAggregate ( ) ) { return true ; } return every ( specM . specQuery . encodings , ( encQ , index ) => { if ( isValueQuery ( encQ ) || isDisabledAutoCountQuery ( encQ ) ) return true ; if ( encQ . channel === CHANNEL . DETAIL ) { if ( specM . wildcardIndex . hasEncodingProperty ( index , Property . CHANNEL ) || opt . constraintManuallySpecifiedValue ) { return false ; } } return true ; } ) ; } , } , { name : 'omitRepeatedField' , description : 'Each field should be mapped to only one channel' , properties : [ Property . FIELD ] , allowWildcardForProperties : true , strict : false , satisfy : ( specM , _ , opt ) => { const fieldUsed = { } ; const fieldEnumerated = { } ; const encodings = specM . specQuery . encodings ; for ( let i = 0 ; i < encodings . length ; i ++ ) { const encQ = encodings [ i ] ; if ( isValueQuery ( encQ ) || isAutoCountQuery ( encQ ) ) continue ; let field ; if ( encQ . field && ! isWildcard ( encQ . field ) ) { field = encQ . field as string ; } if ( isAutoCountQuery ( encQ ) && ! isWildcard ( encQ . autoCount ) ) { field = 'count_*' ; } if ( field ) { if ( specM . wildcardIndex . hasEncodingProperty ( i , Property . FIELD ) ) { fieldEnumerated [ field ] = true ; } if ( fieldUsed [ field ] ) { if ( fieldEnumerated [ field ] || opt . constraintManuallySpecifiedValue ) { return false ; } } fieldUsed [ field ] = true ; } } return true ; } , } , { name : 'omitVerticalDotPlot' , description : 'Do not output vertical dot plot.' , properties : [ Property . CHANNEL ] , allowWildcardForProperties : true , strict : false , satisfy : ( specM , _ , __ ) => { const encodings = specM . getEncodings ( ) ; if ( encodings . length === 1 && encodings [ 0 ] . channel === CHANNEL . Y ) { return false ; } return true ; } , } , { name : 'hasAppropriateGraphicTypeForMark' , description : 'Has appropriate graphic type for mark' , properties : [ Property . CHANNEL , Property . MARK , Property . TYPE , Property . TIMEUNIT , Property . BIN , Property . AGGREGATE , Property . AUTOCOUNT , ] , allowWildcardForProperties : false , strict : false , satisfy : ( specM , _ , __ ) => { const mark = specM . getMark ( ) ; switch ( mark ) { case MARK . AREA : case MARK . LINE : if ( specM . isAggregate ( ) ) { const xEncQ = specM . getEncodingQueryByChannel ( CHANNEL . X ) ; const yEncQ = specM . getEncodingQueryByChannel ( CHANNEL . Y ) ; const xIsMeasure = isMeasure ( xEncQ ) ; const yIsMeasure = isMeasure ( yEncQ ) ; return ( xEncQ && yEncQ && xIsMeasure !== yIsMeasure && ! ( isFieldQuery ( xEncQ ) && ! xIsMeasure && contains ( [ 'nominal' , 'key' ] , xEncQ . type ) ) && ! ( isFieldQuery ( yEncQ ) && ! yIsMeasure && contains ( [ 'nominal' , 'key' ] , yEncQ . type ) ) ) ; } return true ; case MARK . TEXT : return true ; case MARK . BAR : case MARK . TICK : if ( specM . channelEncodingField ( CHANNEL . SIZE ) ) { return false ; } else { const xEncQ = specM . getEncodingQueryByChannel ( CHANNEL . X ) ; const yEncQ = specM . getEncodingQueryByChannel ( CHANNEL . Y ) ; const xIsMeasure = isMeasure ( xEncQ ) ; const yIsMeasure = isMeasure ( yEncQ ) ; if ( xIsMeasure !== yIsMeasure ) { return true ; } return false ; } case MARK . RECT : const xEncQ = specM . getEncodingQueryByChannel ( CHANNEL . X ) ; const yEncQ = specM . getEncodingQueryByChannel ( CHANNEL . Y ) ; const xIsDimension = isDimension ( xEncQ ) ; const yIsDimension = isDimension ( yEncQ ) ; const colorEncQ = specM . getEncodingQueryByChannel ( CHANNEL . COLOR ) ; const colorIsQuantitative = isMeasure ( colorEncQ ) ; const colorIsOrdinal = isFieldQuery ( colorEncQ ) ? colorEncQ . type === TYPE . ORDINAL : false ; const correctChannels = ( xIsDimension && yIsDimension ) || ( xIsDimension && ! specM . channelUsed ( CHANNEL . Y ) ) || ( yIsDimension && ! specM . channelUsed ( CHANNEL . X ) ) ; const correctColor = ! colorEncQ || ( colorEncQ && ( colorIsQuantitative || colorIsOrdinal ) ) ; return correctChannels && correctColor ; case MARK . CIRCLE : case MARK . POINT : case MARK . SQUARE : case MARK . RULE : return true ; } throw new Error ( `hasAllRequiredChannelsForMark not implemented for mark${ mark }` ) ; } , } , { name : 'omitInvalidStackSpec' , description : 'If stack is specified, must follow Vega-Lite stack rules' , properties : [ Property . STACK , Property . FIELD , Property . CHANNEL , Property . MARK , Property . AGGREGATE , Property . AUTOCOUNT , Property . SCALE , getEncodingNestedProp ( 'scale' , 'type' ) , Property . TYPE , ] , allowWildcardForProperties : false , strict : true , satisfy : ( specM , _ , __ ) => { if ( ! specM . wildcardIndex . hasProperty ( Property . STACK ) ) { return true ; } const stackProps = specM . getVlStack ( ) ; if ( stackProps === null && specM . getStackOffset ( ) !== null ) { return false ; } if ( stackProps . fieldChannel !== specM . getStackChannel ( ) ) { return false ; } return true ; } , } , { name : 'omitNonSumStack' , description : 'Stack specifications that use non-summative aggregates should be omitted (even implicit ones)' , properties : [ Property . CHANNEL , Property . MARK , Property . AGGREGATE , Property . AUTOCOUNT , Property . SCALE , getEncodingNestedProp ( 'scale' , 'type' ) , Property . TYPE , ] , allowWildcardForProperties : false , strict : true , satisfy : ( specM , _ , __ ) => { const specStack = specM . getVlStack ( ) ; if ( specStack != null ) { const stackParentEncQ = specM . getEncodingQueryByChannel ( specStack . fieldChannel ) as FieldQuery ; if ( ! contains ( SUM_OPS , stackParentEncQ . aggregate ) ) { return false ; } } return true ; } , } , { name : 'omitTableWithOcclusionIfAutoAddCount' , description : 'Plots without aggregation or autocount where x and y are both discrete should be omitted if autoAddCount is enabled as they often lead to occlusion' , properties : [ Property . CHANNEL , Property . TYPE , Property . TIMEUNIT , Property . BIN , Property . AGGREGATE , Property . AUTOCOUNT , ] , allowWildcardForProperties : false , strict : false , satisfy : ( specM , _ , opt ) => { if ( opt . autoAddCount ) { const xEncQ = specM . getEncodingQueryByChannel ( 'x' ) ; const yEncQ = specM . getEncodingQueryByChannel ( 'y' ) ; if ( ( ! isFieldQuery ( xEncQ ) || isDimension ( xEncQ ) ) && ( ! isFieldQuery ( yEncQ ) || isDimension ( yEncQ ) ) ) { if ( ! specM . isAggregate ( ) ) { return false ; } else { return every ( specM . getEncodings ( ) , ( encQ ) => { const channel = encQ . channel ; if ( channel !== CHANNEL . X && channel !== CHANNEL . Y && channel !== CHANNEL . ROW && channel !== CHANNEL . COLUMN ) { if ( isFieldQuery ( encQ ) && ! encQ . aggregate ) { return false ; } } return true ; } ) ; } } } return true ; } , } , ] . map ( ( sc ) => new SpecConstraintModel ( sc ) ) ; export const SPEC_CONSTRAINT_INDEX = SPEC_CONSTRAINTS . reduce ( ( m , c ) => { m [ c . name ( ) ] = c ; return m ; } , { } ) ; const SPEC_CONSTRAINTS_BY_PROPERTY = SPEC_CONSTRAINTS . reduce ( ( index , c ) => { for ( const prop of c . properties ( ) ) { index . set ( prop , index . get ( prop ) || [ ] ) ; index . get ( prop ) . push ( c ) ; } return index ; } , new PropIndex < SpecConstraintModel [ ] > ( ) ) ; /**
 * Check all encoding constraints for a particular property and index tuple
 */ export function checkSpec ( prop , wildcard , specM , schema , opt ) { const specConstraints = SPEC_CONSTRAINTS_BY_PROPERTY . get ( prop ) || [ ] ; for ( const c of specConstraints ) { if ( c . strict ( ) || ! ! opt [ c . name ( ) ] ) { const satisfy = c . satisfy ( specM , schema , opt ) ; if ( ! satisfy ) { const violatedConstraint = `(spec) ${ c . name ( ) }` ; if ( opt . verbose ) { console . log ( `${ violatedConstraint } failed with ${ specM . toShorthand ( ) } for ${ wildcard . name }` ) ; } return violatedConstraint ; } } } return null ; }