递归过滤嵌套对象

时间:2019-10-06 18:54:30

标签: javascript algorithm object recursion

我有一个看起来像这样的对象:

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,
        },
      },
    },
  },
};

我想创建一个函数(getRoutes),该函数根据传入的RESTRICTIONS来过滤/减少该对象,所有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'

3 个答案:

答案 0 :(得分:2)

首先,在不考虑递归内容的情况下,请确保已正确定义规则逻辑。

我尝试使用您所需的API编写验证功能,但认为它不易读。您可能需要稍后对其进行重构。 (提示:编写一些单元测试!)

下面的示例从您的树中获取一个规则配置对象和一个节点。它返回一个布尔值,指示节点是否符合要求。

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对象内部的条目的问题。

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列出未通过验证的值,
  • 第二,我们检查是否需要担心任何子路线。

如果存在子路由,我们将创建一个具有过滤的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 (i.e. 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,
          },
        },
      },
    },
  };
};

我希望这个例子可以帮助您入门,并让您能够编写自己的/抛光版本。让我知道是否错过任何要求。

答案 1 :(得分:0)

我像这样“解决”它:

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()解决方案将是最好的解决方案,但也许不可能。

答案 2 :(得分:0)

我的版本在算法上与user3297291的版本没有区别。但是代码设计有些不同。

我尝试在对象遍历和匹配测试中都更加通用。我希望两者都是可重用的功能。遍历采用一个谓词和一个子代的属性名称(在您的情况下,'ROUTES')供子代递归,并返回一个过滤提供给它的对象的函数。

对于谓词,我将调用matchesRestrictions的结果与您的USER_RESTRICTIONS对象类似。认为可能还会有其他限制。我假设如果值是布尔值,那么该对象的键值必须具有相同的布尔值。如果是数组,则其中的每个项目都必须在该键处出现在数组中。添加其他类型很容易。但是,这可能太通用了。我真的不知道在USER_PERMMISSIONSRESTRICTIONS部分中还会出现什么。

这是我想出的代码:

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最终是如何。但是我确实用另一个对象和通往孩子的不同路径对其进行了测试:

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}}

因此至少可以重用。

相关问题