Skip to content

Commit

Permalink
feat: add logical operators (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
rqbazan authored Nov 7, 2021
1 parent e132fe7 commit dea25e5
Show file tree
Hide file tree
Showing 10 changed files with 347 additions and 57 deletions.
12 changes: 12 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"esmodules": true
}
}
]
]
}
16 changes: 16 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Node.js: Debug TSDX Jest Tests",
"type": "node",
"request": "launch",
"runtimeArgs": ["--inspect-brk", "${workspaceRoot}/node_modules/tsdx/dist/index.js", "test", "--runInBand"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib"
}
84 changes: 75 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,70 @@ declare module 'toggled' {
}
```

### `FlagQuery` <sup>_Type_</sup>

It could be the feature slug or an flag queries array or more powerful, an object query.

#### Specification

```ts
type FlagQuery =
| string
| FlagQuery[]
| {
[slug: string]: boolean
[operator: symbol]: FlagQuery[]
}
```
#### Example
```tsx
// src/constants/domain.ts
import { Op } from 'toggled'

// Note that each entry is a `FlagQuery`
export const flagQueries: Record<string, FlagQuery> = {
// True if the slug is in the context
FF_1: 'ff-1',

// True if all the slugs are in the context
FF_2_FULL: ['ff-2.1', 'ff-2.2'],

// True if `'ff-2.1'` is in the context and `'ff-2.2'` is not
FF_2_1_ONLY: {
'ff-2.1': true,
'ff-2.2': false,
},

// True if `'ff-3.1'` **or** `'ff-3.2'` is in the context
FF_3_X: {
[Op.OR]: ['ff-3.1', 'ff-3.2'],
},

// True if `'ff-4.1'` **and** `'ff-4.2'` are in the context
FF_4_FULL: {
[Op.AND]: ['ff-4.1', 'ff-4.2'],
},

// True if all the previous queries are true
COMPLEX: {
FF_1: 'ff-1',
FF_2_FULL: ['ff-2.1', 'ff-2.2'],
FF_2_1_ONLY: {
'ff-2.1': true,
'ff-2.2': false,
},
FF_3_X: {
[Op.OR]: ['ff-3.1', 'ff-3.2'],
},
FF_4_FULL: {
[Op.AND]: ['ff-4.1', 'ff-4.2'],
},
},
}
```

### `FeatureContext`

Library context, exported for no specific reason, avoid using it and prefer the custom hooks, or open a PR to add a new one that obligates you to use the `FeatureContext`.
Expand Down Expand Up @@ -140,17 +204,13 @@ function App() {

### `useFlagQuery`

Hook that is used to get the magic function that can process a _feature query_ (FQ), which could be just the feature slug or, and more powerful, one object where the keys are slugs and the values flags.
Hook that is used to get the magic function that can process a _flag query_.

#### Specification

```ts
interface FlagQuery {
(query: string | { [slug: string]: boolean }): boolean
}

interface UseFlagQuery {
(): FlagQuery
(): (query: FlagQuery) => boolean
}
```

Expand All @@ -170,6 +230,8 @@ export default function App() {
}
```

> For more use cases, [please go to the tests.](./test/index.spec.tsx)

### `useFlag`

Hook that is used to get a binary output based on the existence of a feature in the context. So, if the feature is in the context then the flag will be `true`, otherwise `false`.
Expand All @@ -180,7 +242,7 @@ Hook that is used to get a binary output based on the existence of a feature in

```ts
interface UseFlag {
(query: string | { [slug: string]: boolean }): boolean
(query: FlagQuery): boolean
}
```
Expand All @@ -192,12 +254,16 @@ import { useFlag } from 'toggled'
export default function App() {
const hasChat = useFlag('chat')

const hasDesignV2 = useFlag({ 'design-v2': true, 'design-v1': false })
const hasDesignV2Only = useFlag({ 'design-v2': true, 'design-v1': false })

return (
<Layout designV2={hasDesignV2}>
<Layout designV2={hasDesignV2Only}>
{hasChat && <ChatWidget>}
</Layout>
)
}
```

## License

MIT © [Ricardo Q. Bazan](https://rcrd.space)
30 changes: 21 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@
"src",
"logo"
],
"keywords": [
"react",
"feature flags",
"flags",
"features",
"hooks",
"typescript",
"types"
],
"engines": {
"node": ">=10"
},
Expand All @@ -42,17 +51,17 @@
},
"devDependencies": {
"@size-limit/preset-small-lib": "^4.11.0",
"@testing-library/react-hooks": "^7.0.0",
"@types/react": "^17.0.9",
"@types/react-dom": "^17.0.6",
"@testing-library/react-hooks": "^7.0.2",
"@types/react": "^17.0.34",
"@types/react-dom": "^17.0.11",
"husky": "^6.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"semantic-release": "^17.4.3",
"size-limit": "^4.11.0",
"tsdx": "^0.14.1",
"tslib": "^2.2.0",
"typescript": "^4.3.2",
"semantic-release": "^17.4.3"
"tslib": "^2.3.1",
"typescript": "^4.4.4"
},
"publishConfig": {
"access": "public"
Expand All @@ -72,11 +81,14 @@
"size-limit": [
{
"path": "dist/toggled.cjs.production.min.js",
"limit": "300 B"
"limit": "500 B"
},
{
"path": "dist/toggled.esm.js",
"limit": "350 B"
"limit": "550 B"
}
]
],
"resolutions": {
"tsdx/typescript": "^4.4.4"
}
}
77 changes: 66 additions & 11 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,29 @@ export interface FeatureProviderProps<F extends DefaultFeature = DefaultFeature>
children: React.ReactNode
}

export type FeatureQuery = string | { [slug: string]: boolean }
export interface FeatureContextValue<F extends DefaultFeature = DefaultFeature> {
cache: Map<string, F>
}

export const Op = {
OR: Symbol('$or'),
AND: Symbol('$and'),
}

const NO_PROVIDER = {}
export type FlagQuery =
| string
| FlagQuery[]
| {
[slug: string]: boolean
[operator: symbol]: FlagQuery[]
}

const NO_PROVIDER = Symbol('No provider')

export const FeatureContext = React.createContext<any>(NO_PROVIDER)
// @ts-ignore
export const FeatureContext = React.createContext<FeatureContextValue>(NO_PROVIDER)

export function FeatureProvider<F extends DefaultFeature>(props: FeatureProviderProps<F>) {
export function FeatureProvider<F extends DefaultFeature = DefaultFeature>(props: FeatureProviderProps<F>) {
const { features, children } = props

const contextValue = React.useMemo(() => {
Expand All @@ -30,14 +46,15 @@ export function FeatureProvider<F extends DefaultFeature>(props: FeatureProvider
function useFFContext() {
const contextValue = React.useContext(FeatureContext)

// @ts-ignore
if (contextValue === NO_PROVIDER) {
throw new Error('Component must be wrapped with FeatureProvider.')
}

return contextValue
}

export function useFeature<F extends DefaultFeature = DefaultFeature>(slug: string): F | undefined {
export function useFeature(slug: string) {
const { cache } = useFFContext()

return cache.get(slug)
Expand All @@ -46,13 +63,51 @@ export function useFeature<F extends DefaultFeature = DefaultFeature>(slug: stri
export function useFlagQuery() {
const { cache } = useFFContext()

return (query: FeatureQuery) => {
if (typeof query === 'string') {
return cache.has(query)
return function fn(flagQuery: FlagQuery) {
if (typeof flagQuery === 'string') {
return cache.has(flagQuery)
}

for (const slug in query) {
if (cache.has(slug) !== query[slug]) {
if (Array.isArray(flagQuery)) {
for (const query of flagQuery) {
if (!fn(query)) {
return false
}
}

return true
}

for (let key of Reflect.ownKeys(flagQuery)) {
let phase: boolean

if (typeof key === 'string') {
phase = cache.has(key) === flagQuery[key]
} else {
if (key === Op.OR) {
phase = false

for (const innerQuery of flagQuery[key]) {
if (fn(innerQuery)) {
phase = true
break
}
}
} else if (key === Op.AND) {
phase = true

for (const innerQuery of flagQuery[key]) {
if (!fn(innerQuery)) {
phase = false
break
}
}
} else {
throw Error('Invalid Operator')
}
}

if (!phase) {
return false
}
}
Expand All @@ -61,7 +116,7 @@ export function useFlagQuery() {
}
}

export function useFlag(query: FeatureQuery) {
export function useFlag(query: FlagQuery) {
const flagQuery = useFlagQuery()

return flagQuery(query)
Expand Down
3 changes: 2 additions & 1 deletion test/fixtures/features.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[
{ "slug": "example-1", "settings": {} },
{ "slug": "example-2", "settings": {} }
{ "slug": "example-2", "settings": {} },
{ "slug": "example-3", "settings": {} }
]
Loading

0 comments on commit dea25e5

Please sign in to comment.