Skip to content

Commit

Permalink
infer options of boolean flags (#40)
Browse files Browse the repository at this point in the history
* infer options of boolean flags

* fix typo
  • Loading branch information
dferber90 authored Jan 20, 2025
1 parent 55fe9b5 commit 708d5e2
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/wicked-feet-vanish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@vercel/flags': patch
---

generatePermutations: infer options of boolean flags
179 changes: 179 additions & 0 deletions packages/flags/src/next/precompute.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { describe, expect, it } from 'vitest';
import { deserialize, type Flag, flag, generatePermutations } from './index';
import crypto from 'node:crypto';
import type { JsonValue } from '../types';

/**
* Helper function to assert the generated permutations.
*
* @param group the group of flags to generate permutations for
* @param expected the expected permutations
*/
async function expectPermutations(
group: Flag<any, any>[],
expected: any[],
filter?: ((permutation: Record<string, JsonValue>) => boolean) | null,
) {
const permutationsPromise = generatePermutations(group, filter);
await expect(permutationsPromise).resolves.toHaveLength(expected.length);

const permutations = await permutationsPromise;
await expect(
Promise.all(permutations.map((p) => deserialize(group, p))),
).resolves.toEqual(expected);
}

describe('generatePermutations', () => {
describe('when flag declares no options', () => {
it('should infer boolean options', async () => {
process.env.FLAGS_SECRET = crypto.randomBytes(32).toString('base64url');

const flagA = flag({ key: 'a', decide: () => false });
await expectPermutations([flagA], [{ a: false }, { a: true }]);
});
});

describe('when flag declares empty options', () => {
it('should not infer any options', async () => {
process.env.FLAGS_SECRET = crypto.randomBytes(32).toString('base64url');

const flagA = flag({ key: 'a', decide: () => false, options: [] });
await expectPermutations([flagA], []);
});
});

describe('when flag declares options', () => {
it('should generate permutations', async () => {
process.env.FLAGS_SECRET = crypto.randomBytes(32).toString('base64url');

const flagA = flag<string>({
key: 'a',
decide: () => 'two',
options: ['one', 'two', 'three'],
});

await expectPermutations(
[flagA],
[{ a: 'one' }, { a: 'two' }, { a: 'three' }],
);
});
});

describe('when flag declares options with a filter', () => {
it('should generate permutations', async () => {
process.env.FLAGS_SECRET = crypto.randomBytes(32).toString('base64url');

const flagA = flag<string>({
key: 'a',
decide: () => 'two',
options: ['one', 'two', 'three'],
});

await expectPermutations(
[flagA],
[{ a: 'two' }],
// the filter passed to generatePermutations()
(permutation) => permutation.a === 'two',
);
});
});

describe('multiple flags with inferred options', () => {
it('should generate permutations', async () => {
process.env.FLAGS_SECRET = crypto.randomBytes(32).toString('base64url');

const flagA = flag({
key: 'a',
decide: () => false,
});

const flagB = flag({
key: 'b',
decide: () => false,
});

await expectPermutations(
[flagA, flagB],
[
{ a: false, b: false },
{ a: true, b: false },
{ a: false, b: true },
{ a: true, b: true },
],
);
});
});

describe('multiple flags with a mix of inferred and declared options', () => {
it('should generate permutations', async () => {
process.env.FLAGS_SECRET = crypto.randomBytes(32).toString('base64url');

const flagA = flag({
key: 'a',
decide: () => false,
});

const flagB = flag({
key: 'b',
decide: () => false,
});

const flagC = flag({
key: 'c',
decide: () => 'two',
options: ['one', 'two', 'three'],
});

await expectPermutations(
[flagA, flagB, flagC],
[
{ a: false, b: false, c: 'one' },
{ a: true, b: false, c: 'one' },
{ a: false, b: true, c: 'one' },
{ a: true, b: true, c: 'one' },

{ a: false, b: false, c: 'two' },
{ a: true, b: false, c: 'two' },
{ a: false, b: true, c: 'two' },
{ a: true, b: true, c: 'two' },

{ a: false, b: false, c: 'three' },
{ a: true, b: false, c: 'three' },
{ a: false, b: true, c: 'three' },
{ a: true, b: true, c: 'three' },
],
);
});

describe('multiple flags with a mix of inferred and declared options, filtered', () => {
it('should generate permutations', async () => {
process.env.FLAGS_SECRET = crypto.randomBytes(32).toString('base64url');

const flagA = flag({
key: 'a',
decide: () => false,
});

const flagB = flag({
key: 'b',
decide: () => false,
});

const flagC = flag({
key: 'c',
decide: () => 'two',
options: ['one', 'two', 'three'],
});

await expectPermutations(
[flagA, flagB, flagC],
[
{ a: false, b: true, c: 'two' },
{ a: true, b: true, c: 'two' },
],
(permutation) => permutation.c === 'two' && permutation.b,
);
});
});
});
});
6 changes: 4 additions & 2 deletions packages/flags/src/next/precompute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,10 @@ export async function generatePermutations(
}

const options = flags.map((flag) => {
// no permutations if you don't declare any options
if (!flag.options) return [];
// infer boolean permutations if you don't declare any options.
//
// to explicitly opt out you need to use "filter"
if (!flag.options) return [false, true];
return flag.options.map((option) => option.value);
});

Expand Down

0 comments on commit 708d5e2

Please sign in to comment.