<div align="center">

### 🖋️
### @byu-oit/policy

###### write and evaluate policies with ease

</div>

<br>

## Motivation

Realistically, there are already tools out there like this; even free, open source solutions! [**So why build another one?**](https://xkcd.com/927/)
1. Acquiring the approval and funding (for the paid, enterprise solutions) could take several years for us due to higher priorities within our organization; our users (and developers) can't wait that long.
2. We already had [a library](https://github.com/byu-oit/ts-claims-engine) that did *most* of what we needed, but it was limited enough to merit an evolution.
3. The demand for this functionality is *very* high! Nearly every system within our organization would be better served by exposing an API to configure policies rather than requiring an *official* project — an exhausting process that makes end users feel trepidatious to request even small changes or features.

This library cannot solve organizational issues, but it can reduce the burden on the developers who primarily deal with the consequences of them.

So, what are you waiting for? Let's write some policies!

## Write your first policy

We can compare values...

```js
import { Condition } from '@byu-oit/policy'

const condition = new Condition()
    .value(1)
    .isGreaterThan(5)
    .build()

console.info(`1 ${condition.evaluate() ? 'is' : 'is not'} greater than 5`)
// 1 is not greater than 5
```

But more often we want to compare data against a threshold.

```js
import { Condition } from '@byu-oit/policy'

const condition = new Condition()
    .reference('age.value')
    .isLessThan(18)
    .build()

const data = { age: { value: 14 } }
console.info(`I am ${condition.evaluate(data) ? '' : 'not'} a minor`)
// I am a minor
```

We can compare values within the data

```js
import { Condition } from '@byu-oit/policy'

const condition = new Condition()
    .reference('visa.allowed')
    .includes({ reference: 'visa.value' })
    .build()

const data = { visa: { value: 'F1', allowed: ['F1', 'J1'] } }
console.info(`We will ${condition.evaluate(data) ? 'accept' : 'not accept'} your visa`)
// We will accept your visa
```

The data may need to be manipulated before it can be compared to something else

```js
import { Condition } from '@byu-oit/policy'
import * as PolicySystem from '@byu-oit/policy/system'

const MILLISECONDS = 1000, SECONDS = 60, MINUTES = 60, HOURS = 24, DAYS = 365

function toAge (value) {
    if (typeof value !== 'number' && typeof value !== 'string' && !(value instanceof Date)) {
        throw new TypeError(`Cannot cast '${String(value)}' to age`)
    }
    const today = Date.now()
    const birthday = new Date(value).getTime()
    if (birthday > today) {
        throw TypeError(`You can't be younger than today!`)
    }
    return Math.floor((birthday - today) * MILLISECONDS * SECONDS * MINUTES * HOURS * DAYS)
}

PolicySystem.Cast(toAge) // Registers a cast by the name of "toAge"

// You must register anonymous functions (e.g. `const toAge = () => {}`) with a name explicitly
// Cast('age', toAge) // Registers the cast with the name "age"

const condition = new Condition()
    .operand({ reference: 'birthday', cast: toAge.name })
    .isGreaterThanOrEqualTo(16)
    .build()

const data = { birthday: '1970-01-01' }
console.info(`You ${condition.evaluate(data) ? 'can' : 'cannot'} drive a car in Utah`)
// You can drive a car in Utah
```

But what about complex conditions?

Let's expand the example above since some localities specify a different driving age.


```js
import { Condition } from '@byu-oit/policy'
import { Cast } from '@byu-oit/policy/system'

function toAge (value) {
    /* See the implementation of this cast in the previous example */
}
Cast(toAge)

const condition = new Condition()
    .some([
        new Condition()
            .operand({ reference: 'birthday', cast: toAge.name })
            .isGreaterThanOrEqualTo(16),
        new Condition()
            .every([
                new Condition()
                    .operand({ reference: 'birthday', cast: toAge.name })
                    .isGreaterThanOrEqualTo(15),
                new Condition()
                    .value(['Idaho'])
                    .equals({ reference: 'state' })
            ])
    ])
    .build()

const data = { birthday: '1970-01-01', state: 'Idaho' }
console.info(`You ${condition.evaluate(data) ? 'can' : 'cannot'} drive a car in ${data.state}`)
// You can drive a car in Idaho
```

Sometimes you need to evaluate a condition against the items in an array. In such scenarios you may want the rule to
pass when all the elements pass the condition...

```js
const data = {
    people: [
        { age: 22 },
        { age: 17 }
    ]
}

const condition = new Condition()
  .forEach(new Condition().reference('age').isGreaterThanOrEqualTo(16), 'people')
  .build()

condition.evaluate(data) // true
```

Or you may want the rule to pass when at least one of the elements satisfies the condition...

```js
const data = {
    test_scores: [
        { composite: 33 },
        { composite: 27 }
    ]
}

const condition = new Condition()
  .find(new Condition()
      .reference('composite')
      .isGreaterThanOrEqualTo(29), 'test_scores')
  .build()

condition.evaluate(data) // true
```

You'll notice in the previous examples that the context was scoped to a property in the data (i.e. `people` or
`test_scores`). You can always pass in a property path reference, or you can pass in a static value.

```js
const testScores = [
    { composite: 33 },
    { composite: 27 }
]

const condition = new Condition()
  .find(new Condition()
      .reference('composite')
      .isGreaterThanOrEqualTo(29), { value: testScores })
  .build()

condition.evaluate(data) // true
```

The resulting data must always be an iterator when using the `forEach` or `find` methods, or the evaluation will result
in a thrown `TypeError`.

```js
const testScores = '33,27'

const condition = new Condition()
  .find(new Condition()
      .reference('composite')
      .isGreaterThanOrEqualTo(29), { value: testScores })
  .build()

condition.evaluate(data) // TypeError
```

Casts work differently for iterable scopes. Instead of casting the entire list, it will apply the cast to each element.

```js
const data = {
    composite_scores: [ '33', '34' ]
}

const condition = new Condition()
  .find(
      new Condition()
        .reference('composite')
        .isGreaterThanOrEqualTo(29),
      { reference: 'composite_scores', cast: 'number' })
  .build()

// Converts the composite_scores to number[]
condition.evaluate(data) // true
```

You can specify a name or description for each condition, which may help you identify and troubleshoot policy issues,
or just help document its usage.

```js
const condition = new Condition()
    .name('Allowed Visa Type')
    .description('The visa type must be one of those in the allowable list.')
    .reference('visa.allowed')
    .includes({ reference: 'visa.value' })
    .build()
```

For convenience, you can build and evaluate a condition with the `evaluate` method which builds the condition object
and then immediately evaluates the context passed in. Do not use this in a production setting as rebuilding the
condition in every execution would slow down your process.

```js
const data = { visa: { allowed: ['J1', 'F1'] }, value: 'J1' }
const hasAllowedVisa = new Condition()
    .name('Allowed Visa Type')
    .description('The visa type must be one of those in the allowable list.')
    .reference('visa.allowed')
    .includes({ reference: 'visa.value' })
    .evaluate(data)
```
