Skip to content

Commit

Permalink
👌 IMPROVE: checkAllSettle extended api (#3)
Browse files Browse the repository at this point in the history
* 👌 IMPROVE: checkAllSettle extended api

* 📖 DOC: clarify policy definitions in checkAllSettle example
  • Loading branch information
rphlmr authored Oct 1, 2024
1 parent 02db776 commit bce02e3
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 49 deletions.
36 changes: 30 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,18 +399,41 @@ Evaluates all the policies with `check` and returns a snapshot with the results.
It's useful to serialize policies.
It takes an array of policies. If a policy does not take an argument, it can be passed as is. Policies that take an argument have to be passed as a tuple with the argument.
```ts
export function checkAllSettle<TPolicies extends PolicyTuple[], TPolicyName extends TPolicies[number][0]["name"]>(
policies: TPolicies
): PoliciesSnapshot<TPolicyName>
type PolicyTuple =
| Policy<string, PolicyConditionNoArg>
| readonly [string, PolicyConditionNoArg]
| readonly [Policy<string, PolicyConditionNoArg>]
| readonly [Policy<string, PolicyConditionWithArg>, any];
type InferPolicyName<TPolicyTuple> = TPolicyTuple extends readonly [infer name, any]
? name extends Policy<infer Name, any>
? Name
: name extends string
? name
: never
: TPolicyTuple extends readonly [Policy<infer Name, any>]
? Name
: TPolicyTuple extends Policy<infer Name, any>
? Name
: never;
type PoliciesSnapshot<TPolicyName extends string> = { [K in TPolicyName]: boolean };

export function checkAllSettle<
const TPolicies extends readonly PolicyTuple[],
TPolicyTuple extends TPolicies[number],
TPolicyName extends InferPolicyName<TPolicyTuple>,
>(policies: TPolicies): PoliciesSnapshot<TPolicyName>
```

Example:
```ts
// TLDR
const snapshot = checkAllSettle([
[guard.post.policy("is my post"), post],
["post has comments", post.comments.length > 0],
[guard.post.policy("is my post"), post], // Policy with argument
["post has comments", post.comments.length > 0], // Implicit policy with no argument
definePolicy("post has likes", post.likes.length > 0), // Policy without argument
]);

// Example
Expand All @@ -436,9 +459,10 @@ const guard = {
const snapshot = checkAllSettle([
[guard.post.policy("is my post"), post],
["post has comments", post.comments.length > 0],
definePolicy("post has likes", post.likes.length > 0),
]);

console.log(snapshot); // { "is my post": boolean; "post has comments": boolean; }
console.log(snapshot); // { "is my post": boolean; "post has comments": boolean; "post has likes": boolean }
console.log(snapshot["is my post"]) // boolean
```
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "comply",
"version": "0.3.1",
"version": "0.4.0",
"description": "Comply is a tiny library to help you define policies in your app",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
Expand Down
75 changes: 51 additions & 24 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -895,14 +895,14 @@ describe("Check all settle", () => {
it("should snapshot policies", () => {
const PostPolicies = definePolicies((context: Context) => {
const myPostPolicy = definePolicy(
"my post",
"shared policy within policy set",
(post: Post) => post.userId === context.userId,
() => new Error("Not the author")
);

return [
myPostPolicy,
definePolicy("all my published posts", (post: Post) =>
definePolicy("policy from policy set", (post: Post) =>
and(check(myPostPolicy, post), post.status === "published")
),
];
Expand All @@ -913,36 +913,63 @@ describe("Check all settle", () => {
};

const snapshot = checkAllSettle([
[definePolicy("is not null", notNull), "not null"],
[definePolicy("is true", true)],
["post has comments", true],
["post has likes", () => true],
[guard.post.policy("my post"), { userId: "1", comments: [], status: "published" }],
[guard.post.policy("all my published posts"), { userId: "1", comments: [], status: "published" }],
[definePolicy("policy with arg", notNull), "not null"],
["implicit policy with arg", notNull, "not null"],
[definePolicy("policy with no arg", true)],
definePolicy("policy with no arg simple version", true),
["implicit policy with boolean", true],
["implicit policy with no arg function", () => true],
[guard.post.policy("shared policy within policy set"), { userId: "1", comments: [], status: "published" }],
[guard.post.policy("policy from policy set"), { userId: "1", comments: [], status: "published" }],
]);

expect(snapshot).toStrictEqual({
"is not null": true,
"is true": true,
"post has comments": true,
"post has likes": true,
"my post": true,
"all my published posts": true,
"policy with arg": true,
"implicit policy with arg": true,
"policy with no arg": true,
"policy with no arg simple version": true,
"implicit policy with boolean": true,
"implicit policy with no arg function": true,
"shared policy within policy set": true,
"policy from policy set": true,
});

expectTypeOf(snapshot).toEqualTypeOf<{
"is not null": boolean;
"is true": boolean;
"post has comments": boolean;
"post has likes": boolean;
"my post": boolean;
"all my published posts": boolean;
"policy with arg": boolean;
"implicit policy with arg": boolean;
"policy with no arg": boolean;
"policy with no arg simple version": boolean;
"implicit policy with boolean": boolean;
"implicit policy with no arg function": boolean;
"shared policy within policy set": boolean;
"policy from policy set": boolean;
}>();

/** @ts-expect-error */
expectTypeOf(checkAllSettle([[definePolicy("is not null", notNull)]])).toEqualTypeOf<{ [x: string]: boolean }>();
/** @ts-expect-error */
expectTypeOf(checkAllSettle([[definePolicy("is true", true), "extra arg"]])).toEqualTypeOf<{
expectTypeOf(
checkAllSettle([
[
/** @ts-expect-error */
definePolicy("is not null", notNull),
],
])
).toEqualTypeOf<{ [x: string]: boolean }>();
expectTypeOf(
checkAllSettle([
[
/** @ts-expect-error */
definePolicy("is true", true),
"extra arg",
],
])
).toEqualTypeOf<{
[x: string]: boolean;
}>();
expectTypeOf(
checkAllSettle([
/** @ts-expect-error */
definePolicy("is true", (v: unknown) => true),
])
).toEqualTypeOf<{
[x: string]: boolean;
}>();
});
Expand Down
55 changes: 39 additions & 16 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -590,19 +590,23 @@ export function check<TPolicyCondition extends PolicyCondition>(
}

type PolicyTuple =
| Policy<string, PolicyConditionNoArg>
| readonly [string, PolicyConditionNoArg]
| readonly [string, PolicyConditionWithArg, any]
| readonly [Policy<string, PolicyConditionNoArg>]
| readonly [Policy<string, PolicyConditionWithArg>, any];

type InferPolicyName<TPolicyTuple> = TPolicyTuple extends readonly [infer name, any]
? name extends Policy<infer Name, any>
type InferPolicyName<TPolicyTuple> = TPolicyTuple extends readonly [infer NameOrPolicy, ...any[]]
? NameOrPolicy extends Policy<infer Name, any>
? Name
: name extends string
? name
: NameOrPolicy extends string
? NameOrPolicy
: never
: TPolicyTuple extends readonly [Policy<infer Name, any>]
? Name
: never;
: TPolicyTuple extends Policy<infer Name, any>
? Name
: never;

type PoliciesSnapshot<TPolicyName extends string> = { [K in TPolicyName]: boolean };

Expand All @@ -611,14 +615,17 @@ type PoliciesSnapshot<TPolicyName extends string> = { [K in TPolicyName]: boolea
*
* It evaluates all the policies with `check`
*
* If a policy does not take an argument, it can be passed as is. Policies that take an argument have to be passed as a tuple with the argument.
*
* @param policies - A tuple of policies and their arguments (if needed)
*
* @example
* ```ts
* // TLDR
const snapshot = checkAllSettle([
[guard.post.policy("is my post"), post],
["post has comments", post.comments.length > 0],
[guard.post.policy("is my post"), post], // Policy with argument
["post has comments", post.comments.length > 0], // Implicit policy with no argument
definePolicy("post has likes", post.likes.length > 0), // Policy without argument. Can be used as is
]);
// Example
Expand All @@ -642,11 +649,12 @@ type PoliciesSnapshot<TPolicyName extends string> = { [K in TPolicyName]: boolea
};
const snapshot = checkAllSettle([
[guard.post.policy("is my post"), post],
["post has comments", post.comments.length > 0],
[guard.post.policy("is my post"), post], // A policy with an argument
["post has comments", post.comments.length > 0], // An implicit policy with no argument
definePolicy("post has likes", post.likes.length > 0), // A policy without argument. Can be used as is
]);
console.log(snapshot); // { "is my post": boolean; "post has comments": boolean; }
console.log(snapshot); // { "is my post": boolean; "post has comments": boolean; "post has likes": boolean }
console.log(snapshot["is my post"]) // boolean
* ```
*/
Expand All @@ -656,12 +664,27 @@ export function checkAllSettle<
TPolicyName extends InferPolicyName<TPolicyTuple>,
>(policies: TPolicies): PoliciesSnapshot<TPolicyName> {
return policies.reduce(
(acc, policyTuple) => {
const [policyOrName, arg] = policyTuple;
const policyName = typeof policyOrName === "string" ? policyOrName : policyOrName.name;

acc[policyName as TPolicyName] =
typeof policyOrName === "string" ? (typeof arg === "function" ? arg() : arg) : policyOrName.check(arg);
(acc, policyOrTuple) => {
let policyName: string;
let result: boolean;

if (policyOrTuple instanceof Policy) {
// Policy without argument
policyName = policyOrTuple.name;
result = policyOrTuple.check();
} else {
// Policy with argument
const [policyOrName, conditionOrArg, implicitPolicyArg] = policyOrTuple;
policyName = typeof policyOrName === "string" ? policyOrName : policyOrName.name;
result =
typeof policyOrName === "string"
? typeof conditionOrArg === "function"
? conditionOrArg(implicitPolicyArg)
: conditionOrArg
: policyOrName.check(conditionOrArg);
}

acc[policyName as TPolicyName] = result;

return acc;
},
Expand Down

0 comments on commit bce02e3

Please sign in to comment.