import { mapMap , iterableFirst , setIntersect , hashCodeOf , areEqual , mapMergeWithInto , definedMap , addHashCode , setUnionInto } from "collection-utils" ; import { TypeAttributeKind } from "./TypeAttributes" ; import { defined , assert } from "../support/Support" ; import { StringTypeMapping , stringTypeMappingGet } from "../TypeBuilder" ; import { TransformedStringTypeKind } from "../Type" ; import { DateTimeRecognizer } from "../DateTime" ; export class StringTypes { static readonly unrestricted : StringTypes = new StringTypes ( undefined , new Set ( ) ) ; static fromCase ( s , count ) { const caseMap = { } ; caseMap [ s ] = count ; return new StringTypes ( new Map ( [ [ s , count ] as [ string , number ] ] ) , new Set ( ) ) ; } static fromCases ( cases ) { const caseMap = { } ; for ( const s of cases ) { caseMap [ s ] = 1 ; } return new StringTypes ( new Map ( cases . map ( s => [ s , 1 ] as [ string , number ] ) ) , new Set ( ) ) ; } constructor ( readonly cases , readonly transformations ) { if ( cases === undefined ) { assert ( transformations . size === 0 , "We can't have an unrestricted string that also allows transformations" ) ; } } get isRestricted ( ) : boolean { return this . cases !== undefined ; } union ( othersArray , startIndex ) { if ( this . cases === undefined ) return this ; const cases = new Map ( this . cases ) ; const transformations = new Set ( this . transformations ) ; for ( let i = startIndex ; i < othersArray . length ; i ++ ) { const other = othersArray [ i ] ; if ( other . cases === undefined ) return other ; mapMergeWithInto ( cases , ( x , y ) => x + y , other . cases ) ; setUnionInto ( transformations , other . transformations ) ; } return new StringTypes ( cases , transformations ) ; } intersect ( othersArray , startIndex ) { let cases = this . cases ; let transformations = this . transformations ; for ( let i = startIndex ; i < othersArray . length ; i ++ ) { const other = othersArray [ i ] ; if ( cases === undefined ) { cases = definedMap ( other . cases , m => new Map ( m ) ) ; } else if ( other . cases !== undefined ) { const thisCases = cases ; const otherCases = other . cases ; cases = mapMap ( setIntersect ( thisCases . keys ( ) , new Set ( otherCases . keys ( ) ) ) . entries ( ) , k => Math . min ( defined ( thisCases . get ( k ) ) , defined ( otherCases . get ( k ) ) ) ) ; } transformations = setIntersect ( transformations , other . transformations ) ; } return new StringTypes ( cases , transformations ) ; } applyStringTypeMapping ( mapping ) { if ( ! this . isRestricted ) return this ; const kinds = new Set < TransformedStringTypeKind > ( ) ; for ( const kind of this . transformations ) { const mapped = stringTypeMappingGet ( mapping , kind ) ; if ( mapped === "string" ) return StringTypes . unrestricted ; kinds . add ( mapped ) ; } return new StringTypes ( this . cases , new Set ( kinds ) ) ; } equals ( other ) { if ( ! ( other instanceof StringTypes ) ) return false ; return areEqual ( this . cases , other . cases ) && areEqual ( this . transformations , other . transformations ) ; } hashCode ( ) { let h = hashCodeOf ( this . cases ) ; h = addHashCode ( h , hashCodeOf ( this . transformations ) ) ; return h ; } toString ( ) { const parts = [ ] ; const enumCases = this . cases ; if ( enumCases === undefined ) { parts . push ( "unrestricted" ) ; } else { const firstKey = iterableFirst ( enumCases . keys ( ) ) ; if ( firstKey === undefined ) { parts . push ( "enum with no cases" ) ; } else { parts . push ( `${ enumCases . size . toString ( ) } enums: ${ firstKey } (${ enumCases . get ( firstKey ) }), ...` ) ; } } return parts . concat ( Array . from ( this . transformations ) ) . join ( "," ) ; } } class StringTypesTypeAttributeKind extends TypeAttributeKind < StringTypes > { constructor ( ) { super ( "stringTypes" ) ; } get inIdentity ( ) : boolean { return true ; } requiresUniqueIdentity ( st ) { return st . cases !== undefined && st . cases . size > 0 ; } combine ( arr ) { assert ( arr . length > 0 ) ; return arr [ 0 ] . union ( arr , 1 ) ; } intersect ( arr ) { assert ( arr . length > 0 ) ; return arr [ 0 ] . intersect ( arr , 1 ) ; } makeInferred ( _ ) { return undefined ; } stringify ( st ) { return st . toString ( ) ; } } export const stringTypesTypeAttributeKind = new StringTypesTypeAttributeKind ( ) ; const INTEGER_STRING = /^(0|-?[1-9]\d*)$/ ; const MIN_INTEGER_STRING = 1 << 31 ; const MAX_INTEGER_STRING = - ( MIN_INTEGER_STRING + 1 ) ; function isIntegerString ( s ) { if ( s . match ( INTEGER_STRING ) === null ) { return false ; } const i = parseInt ( s , 10 ) ; return i >= MIN_INTEGER_STRING && i <= MAX_INTEGER_STRING ; } const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ ; function isUUID ( s ) { return s . match ( UUID ) !== null ; } const URI = /^(https?|ftp):\/\/[^{}]+$/ ; function isURI ( s ) { return s . match ( URI ) !== null ; } param s export function inferTransformedStringTypeKindForString ( s , recognizer ) { if ( s . length === 0 || "0123456789-abcdefth" . indexOf ( s [ 0 ] ) < 0 ) return undefined ; if ( recognizer . isDate ( s ) ) { return "date" ; } else if ( recognizer . isTime ( s ) ) { return "time" ; } else if ( recognizer . isDateTime ( s ) ) { return "date-time" ; } else if ( isIntegerString ( s ) ) { return "integer-string" ; } else if ( s === "false" || s === "true" ) { return "bool-string" ; } else if ( isUUID ( s ) ) { return "uuid" ; } else if ( isURI ( s ) ) { return "uri" ; } return undefined ; }