[英]Filter nested object recursively
我有一个看起来像这样的 object:
const ROUTES = {
ACCOUNT: {
TO: '/account',
RESTRICTIONS: {
shouldBeLoggedIn: true,
},
ROUTES: {
PROFILE: {
TO: '/account/profile',
RESTRICTIONS: {
shouldBeLoggedIn: true,
},
ROUTES: {
INFORMATION: {
TO: '/account/profile/information',
RESTRICTIONS: {
shouldBeLoggedIn: true,
permissions: ['EMAIL'],
},
},
PASSWORD: {
TO: '/account/profile/password',
RESTRICTIONS: {
shouldBeLoggedIn: true,
permissions: ['EMAIL', 'ADMIN'],
},
},
},
},
COLLECTIONS: {
TO: '/account/collections',
RESTRICTIONS: {
shouldBeLoggedIn: true,
permissions: ['ADMIN'],
},
},
LIKES: {
TO: '/account/likes',
RESTRICTIONS: {
shouldBeLoggedIn: true,
},
},
},
},
};
我想创建一个 function ( getRoutes
),它根据传入的RESTRICTIONS
过滤/减少 object,所有permissions
必须匹配。
function getRoutes(routes, restrictions){
//...
}
const USER_RESTRICTIONS = {
shouldBeLoggedIn: true,
permissions: ['EMAIL'],
}
const allowedRoutes = getRoutes(ROUTES, USER_RESTRICTIONS)
allowedRoutes === {
ACCOUNT: {
TO: '/account',
RESTRICTIONS: {
shouldBeLoggedIn: true,
},
ROUTES: {
PROFILE: {
TO: '/account/profile',
RESTRICTIONS: {
shouldBeLoggedIn: true,
},
ROUTES: {
INFORMATION: {
TO: '/account/profile/information',
RESTRICTIONS: {
shouldBeLoggedIn: true,
permissions: ['EMAIL'],
},
},
},
},
LIKES: {
TO: '/account/likes',
RESTRICTIONS: {
shouldBeLoggedIn: true,
},
},
},
},
} ? 'YAY' : 'NAY'
首先,不用考虑递归的东西,确保你的规则逻辑定义得很好。
我尝试使用您所需的 API 编写验证 function,但不认为它非常易读。 您可能希望稍后对其进行重构。 (提示:编写一些单元测试!)
下面的示例采用规则配置 object 和树中的节点。 它返回一个 boolean 指示节点是否符合要求。
const includedIn = xs => x => xs.includes(x); // RuleSet -> Path -> bool const isAllowed = ({ shouldBeLoggedIn = false, permissions = [] }) => ({ RESTRICTIONS }) => ( (shouldBeLoggedIn? RESTRICTIONS.shouldBeLoggedIn: true) && RESTRICTIONS.permissions.every(includedIn(permissions)) ); console.log( [ { RESTRICTIONS: { shouldBeLoggedIn: true, permissions: [ ] } }, { RESTRICTIONS: { shouldBeLoggedIn: true, permissions: [ 'EMAIL' ] } }, { RESTRICTIONS: { shouldBeLoggedIn: true, permissions: [ 'EMAIL', 'ADMIN' ] } } ].map( isAllowed({ shouldBeLoggedIn: true, permissions: [ 'EMAIL'] }) ) )
将这段代码排序后,就可以开始思考如何遍历树了。 您基本上定义的是如何遍历每条路径以及何时返回。
如果我们只是想记录,只需 (1) 检查ROUTES
和 (2) 遍历v.ROUTES
object 中的条目。
const traverse = obj => { Object.entries(obj).forEach( ([k, v]) => { console.log(v.TO); if (v.ROUTES) traverse(v.ROUTES) } ) }; traverse(getRoutes()); function getRoutes() { return { ACCOUNT: { TO: '/account', RESTRICTIONS: { shouldBeLoggedIn: true, }, ROUTES: { PROFILE: { TO: '/account/profile', RESTRICTIONS: { shouldBeLoggedIn: true, }, ROUTES: { INFORMATION: { TO: '/account/profile/information', RESTRICTIONS: { shouldBeLoggedIn: true, permissions: ['EMAIL'], }, }, PASSWORD: { TO: '/account/profile/password', RESTRICTIONS: { shouldBeLoggedIn: true, permissions: ['EMAIL', 'ADMIN'], }, }, }, }, COLLECTIONS: { TO: '/account/collections', RESTRICTIONS: { shouldBeLoggedIn: true, permissions: ['ADMIN'], }, }, LIKES: { TO: '/account/likes', RESTRICTIONS: { shouldBeLoggedIn: true, }, }, }, }, }; };
然后是最难的部分:创建一个新的树结构。
我选择采取两个步骤:
filter
掉不通过验证的值,如果有子路由,我们创建一个新的路径 object,它具有过滤的 ROUTES 值。
const traverse = (obj, pred) => Object.fromEntries( Object.entries(obj).filter( ([k, v]) => pred(v) // Get rid of the paths that don't match restrictions ).map( ([k, v]) => [ k, v.ROUTES // If there are child paths, filter those as well (ie recurse)? Object.assign({}, v, { ROUTES: traverse(v.ROUTES, pred) }): v ] ) ); const includedIn = xs => x => xs.includes(x); const isAllowed = ({ shouldBeLoggedIn = false, permissions = [] }) => ({ RESTRICTIONS }) => ( (shouldBeLoggedIn? RESTRICTIONS.shouldBeLoggedIn: true) && (RESTRICTIONS.permissions || []).every(includedIn(permissions)) ); console.log( traverse( getRoutes(), isAllowed({ shouldBeLoggedIn: true, permissions: [ 'EMAIL'] }) ) ) function getRoutes() { return { ACCOUNT: { TO: '/account', RESTRICTIONS: { shouldBeLoggedIn: true, }, ROUTES: { PROFILE: { TO: '/account/profile', RESTRICTIONS: { shouldBeLoggedIn: true, }, ROUTES: { INFORMATION: { TO: '/account/profile/information', RESTRICTIONS: { shouldBeLoggedIn: true, permissions: ['EMAIL'], }, }, PASSWORD: { TO: '/account/profile/password', RESTRICTIONS: { shouldBeLoggedIn: true, permissions: ['EMAIL', 'ADMIN'], }, }, }, }, COLLECTIONS: { TO: '/account/collections', RESTRICTIONS: { shouldBeLoggedIn: true, permissions: ['ADMIN'], }, }, LIKES: { TO: '/account/likes', RESTRICTIONS: { shouldBeLoggedIn: true, }, }, }, }, }; };
我希望这个示例可以帮助您入门并让您编写自己的/完善的版本。 如果我错过了任何要求,请告诉我。
我这样“解决”了它:
export const checkLoggedIn = (shouldBeLoggedIn, isAuthenticated) => {
if (!shouldBeLoggedIn) {
return true;
}
return isAuthenticated;
};
function isRouteAllowed(route, restrictions) {
const routeShouldBeLoggedIn = route.RESTRICTIONS.shouldBeLoggedIn;
const passedLoggedInCheck = checkLoggedIn(
routeShouldBeLoggedIn,
restrictions.get('shouldBeLoggedIn')
);
if (!passedLoggedInCheck) {
return false;
} else {
const routePermissions = route.RESTRICTIONS.permissions;
if (!routePermissions) {
return true;
} else {
const passedPermissions = routePermissions.every((permission) => {
const restrictPermissions = restrictions.get('permissions');
return (
restrictPermissions &&
restrictPermissions.find &&
restrictPermissions.find(
(userPermission) => userPermission === permission
)
);
});
return passedLoggedInCheck && passedPermissions;
}
}
}
function forEachRoute(
routes,
restrictions,
routesToDelete = [],
parentPath = []
) {
const routeSize = Object.keys(routes).length - 1;
Object.entries(routes).forEach(([key, route], index) => {
const childRoutes = route.ROUTES;
if (childRoutes) {
parentPath.push(key);
parentPath.push('ROUTES');
forEachRoute(childRoutes, restrictions, routesToDelete, parentPath);
} else {
const allowed = isRouteAllowed(route, restrictions);
if (!allowed) {
const toAdd = [...parentPath, key];
routesToDelete.push(toAdd);
}
}
if (routeSize === index) {
// new parent
parentPath.pop();
parentPath.pop();
}
});
}
const deletePropertyByPath = (object, path) => {
let currentObject = object;
let parts = path.split('.');
const last = parts.pop();
for (const part of parts) {
currentObject = currentObject[part];
if (!currentObject) {
return;
}
}
delete currentObject[last];
};
export function removeRestrictedRoutes(routes, restrictions) {
let routesToDelete = [];
forEachRoute(routes, restrictions, routesToDelete);
let allowedRoutes = routes;
routesToDelete.forEach((path) => {
deletePropertyByPath(allowedRoutes, path.join('.'));
});
return allowedRoutes;
}
像这样使用:
const USER_RESTRICTIONS = {
shouldBeLoggedIn: true,
permissions: ['EMAIL'],
}
const allowedRoutes = getRoutes(ROUTES, USER_RESTRICTIONS)
不是最高效的解决方案,但它有效。 @user3297291 解决方案似乎要好得多,因此将对其进行重构,只需使其更具可读性即可。 我认为.reduce()
的解决方案是最好的解决方案,但也许不可能。
我的版本在算法上与 user3297291 的版本没有什么不同。 但是代码设计有点不同。
我尝试在 object 遍历和匹配测试中都更加通用。 我希望两者都是可重用的功能。 遍历采用谓词和属性名称供子级递归(在您的情况下为'ROUTES'
)并返回 function 过滤提供给它的 object 。
对于谓词,我将调用matchesRestrictions
的结果传递给您的USER_RESTRICTIONS
object。 人们的想法是可能会有其他限制。 我假设如果该值为 boolean,那么 object 必须具有与该键相同的 boolean 值。 如果它是一个数组,那么其中的每个项目都必须出现在数组中的那个键处。 添加其他类型很容易。 不过,这可能太笼统了; 我真的不知道USER_PERMMISSIONS
或RESTRICTIONS
部分中还会出现什么。
这是我想出的代码:
const filterObj = (pred, children) => (obj) => Object.fromEntries ( Object.entries (obj).filter ( ([k, v]) => pred (v)).map ( ([k, v]) => [ k, v [children]? {...v, [children]: filterObj (pred, children) (v [children]) }: v ] ) ) const matchesRestrictions = (config) => ({RESTRICTIONS = {}}) => Object.entries (RESTRICTIONS).every (([key, val]) => typeof val == 'boolean'? config [key] === val: Array.isArray (val)? val.every (v => (config [key] || []).includes (v)): true // What else do you want to handle? ) const ROUTES = {ACCOUNT: {TO: "/account", RESTRICTIONS: {shouldBeLoggedIn: true}, ROUTES: {PROFILE: {TO: "/account/profile", RESTRICTIONS: {shouldBeLoggedIn: true}, ROUTES: {INFORMATION: {TO: "/account/profile/information", RESTRICTIONS: {shouldBeLoggedIn: true, permissions: ["EMAIL"]}}, PASSWORD: {TO: "/account/profile/password", RESTRICTIONS: {shouldBeLoggedIn: true, permissions: ["EMAIL", "ADMIN"]}}}}, COLLECTIONS: {TO: "/account/collections", RESTRICTIONS: {shouldBeLoggedIn: true, permissions: ["ADMIN"]}}, LIKES: {TO: "/account/likes", RESTRICTIONS: {shouldBeLoggedIn: true}}}}}; const USER_RESTRICTIONS = {shouldBeLoggedIn: true, permissions: ['EMAIL']} console.log ( filterObj (matchesRestrictions (USER_RESTRICTIONS), 'ROUTES') (ROUTES) )
我不知道通用filterObj
最终是怎样的。 但我确实用另一个 object 和不同的路径对它进行了测试:
const obj = {x: {foo: 1, val: 20, kids: {a: {foo: 2, val: 15, kids: {b: {foo: 3, val: 8}, c: {foo: 4, val: 17}, d: {foo: 5, val: 12}}}, e: {foo: 6, val: 5, kids: {f: {foo: 7, val: 23}, g: {foo: 8, val: 17}}}, h: {foo: 9, val: 11, kids: {i: {foo: 10, val: 3}, j: {foo: 11, val: 7}}}}}, y: {foo: 12, val: 8}, z: {foo: 13, val: 25, kids: {k: {foo: 14, val: 18, kids: {l: {foo: 5, val: 3}, m: {foo: 11, val: 7}}}}}}
const pred = ({val}) => val > 10
filterObj ( pred, 'kids') (obj)
得到这个结果:
{x: {foo: 1, kids: {a: {foo: 2, kids: {c: {foo: 4, val: 17}, d: {foo: 5, val: 12}}, val: 15}, h: {foo: 9, kids: {}, val: 11}}, val: 20}, z: {foo: 13, kids: {k: {foo: 14, kids: {}, val: 18}}, val: 25}}
所以它至少有点可重用。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.