UNPKG

spyne

Version:

Reactive Real-DOM Framework for Advanced Javascript applications

427 lines (345 loc) 14.4 kB
import { isEmpty, head, values, compose, prop, complement, isNil, allPass, reduce, filter, equals, toPairs, replace, either, propEq, __, invert, path, zipObj, reject, keys, find, assoc, is, has, when, split, always, concat, join, flatten, map, ifElse, test, findLastIndex, last, defaultTo, fromPairs } from 'ramda' import { SpyneAppProperties } from './spyne-app-properties.js' export class SpyneUtilsChannelRouteUrl { constructor() { /** * @module SpyneUtilsChannelRouteUrl * @type internal * * @constructor * @desc * Internal url methods for the SpyneRouteChannel */ this.checkIfObjIsNotEmptyOrNil = SpyneUtilsChannelRouteUrl.checkIfObjIsNotEmptyOrNil.bind(this) } static checkIfObjIsNotEmptyOrNil(obj) { const isNotEmpty = compose(complement(isEmpty), head, values) const isNotNil = compose(complement(isNil), head, values) const isNotNilAndIsNotEmpty = allPass([isNotEmpty, isNotNil]) return isNotNilAndIsNotEmpty(obj) } static checkIfParamValueMatchesRegex(paramValue, routeObj) { const rejectParamKey = reject(equals('routeName')) const keysArr = compose(rejectParamKey, keys) const testForRegexMatch = str => test(new RegExp(str), paramValue) const checker = compose(find(testForRegexMatch), keysArr) const regexMatchStr = checker(routeObj) if (is(String, regexMatchStr)) { routeObj = assoc(paramValue, prop(regexMatchStr, routeObj), routeObj) } return routeObj } static formatStrAsWindowLocation(str) { const hash = str const search = str const pathname = str return { hash, search, pathname } } static getLocationStrByType(t, isHash = false, loc = window.location) { const type = isHash === true ? 'hash' : t const typeMap = { slash: 'pathname', query: 'search', hash: 'hash' } const propVal = typeMap[type] const str = prop(propVal, loc) const checkForSlashAndHash = /^(\/)?(#)?(\/)?(.*)$/ // console.log("DATA LOC STR ",{str, loc, prop, type,isHash}); return str.replace(checkForSlashAndHash, '$4') } static createRouteArrayFromParams(data, route, t = 'slash', paramsFromLoc) { const urlArr = [] const loopThroughParam = (routeObj) => { const urlObj = {} const keyword = routeObj.routeName // PARAM FORM SPYNE CONFIG let paramValFromData = data[keyword] !== undefined ? data[keyword] : prop(keyword, paramsFromLoc) // PULL VALUE FOR THIS PARAM FROM DATA const paramValType = typeof (routeObj[paramValFromData]) if (paramValType === 'string') { paramValFromData = routeObj[paramValFromData] } else if (paramValType === 'undefined') { routeObj = this.checkIfParamValueMatchesRegex(paramValFromData, routeObj) } urlObj[keyword] = paramValFromData if (this.checkIfObjIsNotEmptyOrNil(urlObj)) { urlArr.push(urlObj) } const isObject = is(Object, routeObj) const objectParamExists = has(paramValFromData, routeObj) const objectContainsRoute = has('routePath', routeObj) const recursivelyCallLoopBool = objectParamExists && isObject if (recursivelyCallLoopBool === true) { const newObj = routeObj[paramValFromData] if (has('routePath', newObj)) { loopThroughParam(newObj.routePath) } } else if (objectContainsRoute === true && paramValFromData !== undefined) { loopThroughParam(routeObj.routePath) } } loopThroughParam(route) return urlArr } static createSlashString(arr) { const arrClear = reject(isNil) const notUndefined = when(complement(isNil, __), join('/')) const stripRegex = replace(/^(\^*)(.*|^\$)(\$)$/, '$2') const joiner = compose(stripRegex, notUndefined, arrClear, flatten, map(values)) return joiner(arr) } static createQueryString(arr) { const arrClear = reject(isNil) const isNotNilAndIsNotEmpty = this.checkIfObjIsNotEmptyOrNil const createPair = compose( join('='), flatten, toPairs) const checkPair = ifElse( isNotNilAndIsNotEmpty, createPair, always(undefined) ) const mapArrayOfPairs = map(checkPair) const checkIfStrIsEmpty = when( complement(isEmpty), concat('?')) const createQs = compose( checkIfStrIsEmpty, join('&'), arrClear, mapArrayOfPairs) return createQs(arr) } static checkPayloadForRegexOverrides(urlsArr, data, parseString = 'Value') { // CHECK IF PAYLOAD HAS ANY OVERRIDES const getPropValOverride = (key) => prop(`${key}${parseString}`, data) // GET ANY POSSIBLE VALUE USING THE CURRENT KEY const getOverrideVal = compose(getPropValOverride, head, keys) // MAP ALL OBJECT VALS TO TEST FOR OVERRIDES const mapUrlProps = (prop) => { const overrideVal = getOverrideVal(prop) // console.log("override val ",overrideVal, data); // if regex value is found if (overrideVal !== undefined) { return assoc(compose(head, keys)(prop), overrideVal, prop) } // identity return prop } return map(mapUrlProps, urlsArr) } static checkForRouteValWithMultipleVals(urlsArr) { // Define the internal method to extract the string before the pipe const getStringBeforePipe = (input) => { const match = input.match(/^[^|]*/) return match ? (match[0] === '^$' ? '' : match[0]) : input } const mapUrlArrObj = (obj) => { const pipeReducer = (acc, key) => { acc[key] = getStringBeforePipe(obj[key]) return acc } if (typeof obj === 'object') { obj = Object.keys(obj).reduce(pipeReducer, {}) } return obj } if (Array.isArray(urlsArr) === true) { return urlsArr.map(mapUrlArrObj) } return urlsArr } static checkForRouteValsPartMatch(urlsArr, data = {}) { /** * 1. Loop through urlsArr * 2. Loop through properties * 3. Apply matchGlobPattern to each property value and check for match with data object property * 4. Return the updated object with matched values, or keep the original pattern if no match */ const matchGlobPattern = (str, globPattern) => { const globPatternMatch = (pattern, value) => { const escapedPattern = pattern .replace(/[-/\\^$+?.()|[\]{}]/g, '\\$&') // Escape special characters .replace(/\*/g, '[\\w\\d_-]*') // * should match any sequence of alphanumeric, underscores, or dashes const regexPattern = new RegExp(`^${escapedPattern}$`) // Match the full string return regexPattern.test(value) } // Split the globPattern by pipe (|) const patterns = globPattern.split('|') // Check if the string matches any of the patterns const hasMatch = patterns.some((pattern) => globPatternMatch(pattern, str)) // Return the string if there's a match, otherwise return the glob pattern return hasMatch ? str : globPattern } // Function to check and replace matching properties const replaceMatchingProperty = (obj) => { const updatedObj = { ...obj } // Create a copy of the object to avoid mutation // Function to check each property and apply matchGlobPattern const checkAndReplaceProperty = (key) => { if (Object.prototype.hasOwnProperty.call(updatedObj, key) && data[key]) { updatedObj[key] = matchGlobPattern(data[key], updatedObj[key]) } } // Loop through the properties of the object Object.keys(updatedObj).forEach(checkAndReplaceProperty) return updatedObj } // Loop through the array and replace matching properties return urlsArr.map(replaceMatchingProperty) } static convertParamsToRoute(data, r = SpyneAppProperties.config.channels.ROUTE, t, locStr) { const urlType = t !== undefined ? t : r.type const isHash = r.isHash const route = r.routes.routePath const locationStr = locStr !== undefined ? locStr : this.getLocationStrByType(urlType, isHash) const paramsFromCurrentLocation = this.convertRouteToParams(locationStr, r, urlType).routeData let urlArr = this.createRouteArrayFromParams(data, route, urlType, paramsFromCurrentLocation) urlArr = SpyneUtilsChannelRouteUrl.checkPayloadForRegexOverrides(urlArr, data) urlArr = SpyneUtilsChannelRouteUrl.checkForRouteValWithMultipleVals(urlArr) // THIS WILL MATCH EXACTLY THE PROPERTY ID, SO pageId="dashboard_1" will return that instead of globPattern[0] // urlArr = SpyneUtilsChannelRouteUrl.checkForRouteValsPartMatch(urlArr, data) // console.log('PARAMS TO ROUTE ', { data, r, urlArr, locationStr, paramsFromCurrentLocation }) // THIS CREATES A QUERY PATH STR if (urlType === 'query') { return this.createQueryString(urlArr) } return this.createSlashString(urlArr) } static findIndexOfMatchedStringOrRegex(mainStr, paramsArr) { const is404 = paramsArr.includes('.+') const reFn = (s) => { if (is404) { if (!s.startsWith('^')) { s = '^' + s } if (!s.endsWith('$')) { s = s + '$' } } return new RegExp(s) } const checkForEmpty = replace(/^$/, '^$') const createStrRegexTest = (str) => { return { str, re: reFn(str) } } const checkForEitherStrOrReMatch = either( propEq(mainStr, 'str'), compose(test(__, mainStr), prop('re')) ) const findMatchIndex = compose(findLastIndex(equals(true)), map(checkForEitherStrOrReMatch), map(createStrRegexTest), map(checkForEmpty)) return findMatchIndex(paramsArr) } static checkIfValueShouldMapToParam(obj, str, regexTokens) { // // // THIS IS A VERY IMPORTANT METHOD // IT COMPARES THE STRING TO BE MATCHED WITH THE ROUTE PATH OBJECT // AND IT WILL RETURN ONE OF THREE THINGS // 1. THE STRING ITSELF // 2. A MATCHING KEY FROM THE CURRENT OBJECT // 3. THE ROUTENAME // // GO THROUGH KEYS AND CHECK IF REGEX MATCHES // GO THROUGH ROUTE CONFIG TO FIGURE OUT IF VAL OR KEY SHOULD BE COMPARED // if the value is an object, choose the key of the route path to check against const getValCompareArr = compose(map(last), map(filter(is(String))), toPairs) // CREATE THE ARRAY OF EITHER VALS OR KEYS const paramsArr = getValCompareArr(obj) // RESULTS FROM PARAM CHECK // let paramIndex = checkForParamsMatch(paramsArr); const paramIndex = SpyneUtilsChannelRouteUrl.findIndexOfMatchedStringOrRegex(str, paramsArr) // DEFAULT VAL FOR STRING let paramStr = str // LEGACY METHOD -- TURN OFF // WHEN THERE IS A MATCH FROM THE CURRENT ROUTE PATH OBJECT if (paramIndex >= 0) { const param = paramsArr[paramIndex] const invertedObj = invert(obj) // PULL INVERTED OBJECT TO SEE IF STR MATCHES const getParamInverted = compose(head, defaultTo([]), prop(param)) const paramInverted = getParamInverted(invertedObj) // spyne 11.0.1; const re = /^([-$\w]*)$/ const keyMatch = re.test(paramInverted) if (keyMatch === true && is(String, paramInverted) === true) { paramStr = paramInverted } } return paramStr } static createArrFromSlashStr(str) { const slashRe = /^([/])?(.*)$/ return str.replace(slashRe, '$2').split('/') } static convertSlashRouteStrToParamsObj(topLevelRoute, str, regexTokens) { const routeValue = str const valuesArr = this.createArrFromSlashStr(str) const paths = [] const routedValuesArr = [] let latestObj = topLevelRoute const createParamsFromStr = (total, currentValue) => { const routeValueStr = this.checkIfValueShouldMapToParam(latestObj, currentValue, regexTokens) latestObj = this.checkIfParamValueMatchesRegex(currentValue, latestObj) const prevPathNotEqCurrentPath = (str) => str !== last(paths) const isEmptyOrNil = either(isEmpty, isNil) if (latestObj !== undefined && prevPathNotEqCurrentPath(latestObj.routeName) && isEmptyOrNil(routeValueStr) === false) { paths.push(latestObj.routeName) routedValuesArr.push(routeValueStr) } const strPath = [currentValue, 'routePath'] const routeParamPath = ['routePath'] const objectFromStr = path(strPath, latestObj) const objectFromRouteParam = path(routeParamPath, latestObj) if (objectFromStr !== undefined) { latestObj = objectFromStr } else if (objectFromRouteParam !== undefined) { latestObj = objectFromRouteParam } } reduce(createParamsFromStr, 0, valuesArr) const routeData = zipObj(paths, routedValuesArr) const pathInnermost = this.getLastArrVal(paths) return { paths, pathInnermost, routeData, routeValue } } static getLastArrVal(arr) { const getLastParam = (a) => last(a) !== undefined ? last(a) : '' return getLastParam(arr) } static createDefaultParamFromEmptyStr(topLevelRoute, str) { const obj = {} const keyword = topLevelRoute.routeName obj[keyword] = this.checkIfValueShouldMapToParam(topLevelRoute, str) return obj } static convertQueryStrToParams(topLevelRoute, str, regexTokens) { const queryRe = /^([?])?(.*)$/ const routeValue = str const strArr = str.replace(queryRe, '$2') const convertToParams = compose(map(split('=')), split('&')) const paramsArr = convertToParams(strArr) let routeData = fromPairs(paramsArr) let paths = map(head, paramsArr) if (isEmpty(str) === true) { routeData = this.createDefaultParamFromEmptyStr(topLevelRoute, str, regexTokens) paths = keys(routeData) } const pathInnermost = this.getLastArrVal(paths) return { paths, pathInnermost, routeData, routeValue } } static convertRouteToParams(str, routeConfig, t) { const addSlash = (url) => url.replace(/\/$|$/, '/') if (routeConfig === undefined) { return {} } const type = t !== undefined ? t : routeConfig.type const regexTokens = defaultTo({}, prop('regexTokens', routeConfig)) const topLevelRoute = routeConfig.routes.routePath if (type === 'query') { return this.convertQueryStrToParams(topLevelRoute, str) } str = addSlash(str) return this.convertSlashRouteStrToParamsObj(topLevelRoute, str, regexTokens) } }