# rust_decider

Rust implementation of bucketing, targeting, overrides, and dynamic config logic.

# Usage

## class `Decider`

A class used to expose these APIs:

- ```
  choose(
     feature_name: str,
     context: Mapping[str, JsonValue]
  ) -> Decision
  ```
- ```
  choose_all(
     context: Mapping[str, JsonValue],
     bucketing_field_filter: Optional[str] = None
  ) -> Dict[str, Decision]
  ```

### `choose()` examples:

```python
from rust_decider import Decider
from rust_decider import DeciderException
from rust_decider import FeatureNotFoundException
from rust_decider import DeciderInitException
from rust_decider import PartialLoadException

# initialize Decider instance
try:
    decider = Decider("../cfg.json")
except PartialLoadException as e:
    # log errors of misconfigured features
    print(f"{e.args[0]}: {e.args[2]}")

    # use partially initialized Decider instance
    decider = e.args[1]
except DeciderInitException as e:
    print(e)

# get a Decision for a feature via choose()
try:
    decision = decider.choose(feature_name="exp_1", context={"user_id": "3", "app_name": "ios"})
except DeciderException as e:
    print(e)

assert dict(decision) == {
    "variant": "variant_0",
    "feature_id": 3246,
    "feature_name": "exp_1",
    "feature_version": 2,
    "events": [
      "0::::3246::::exp_1::::2::::variant_0::::3::::user_id::::37173982::::2147483648::::test"
    ]
}

# `user_id` targeting not satisfied so "variant" is `None` in the returned Decision
try:
    decision = decider.choose(feature_name="exp_1", context={"user_id": "1"})
except DeciderException as e:
    print(e)

assert dict(decision) == {
  "variant": None,
  "feature_id": 3246,
  "feature_name": "exp_1",
  "feature_version": 2,
  "events": []
}

# handle "feature not found" exception
# (`FeatureNotFoundException` is a subclass of `DeciderException`)
try:
    decision = decider.choose(feature_name="not_here", context={"user_id": "1"})
except FeatureNotFoundException as e:
  print("handle feature not found exception:")
  print(e)
except DeciderException as e:
    print(e)
```

### `choose_all()` examples:

```python
from rust_decider import Decider
from rust_decider import DeciderException
from rust_decider import DeciderInitException

try:
    decider = Decider("../cfg.json")
except DeciderInitException as e:
    print(e)


decisions = decider.choose_all(context={"user_id": "3", "app_name": "ios"}, bucketing_field_filter="user_id")


assert dict(decisions["exp_67"]) == {
  "variant": "variant_0",
  "value": None,
  "feature_id": 3125,
  "feature_name": "exp_67",
  "feature_version": 4,
  "events": [
    "0::::3125::::exp_67::::4::::variant_0::::3::::user_id::::37173982::::2147483648::::test"
  ]
}
```

## python bindings used in `Decider` class

```python
import rust_decider

# Init decider
decider = rust_decider.init("darkmode overrides targeting holdout mutex_group fractional_availability value", "../cfg.json")

# Bucketing needs a context
ctx = rust_decider.make_ctx({"user_id": "7"})

# Get a decision
choice = decider.choose("exp_1", ctx)
assert choice.err() is None # check for errors
choice.decision() # get the variant

# Get a dynamic config value
dc = decider.get_map("dc_map", ctx) # fetch a map DC
assert dc.err() is None # check for errors
dc.val() # get the actual map itself
```

# Development

## Updating package with latest `src/lib.rs` changes

```sh
# In a virtualenv, python >= 3.7
$ cd decider-py
$ pip install -r requirements-dev.txt
$ maturin develop
```

## Running tests

```sh
$ pytest decider-py/test/
```

## Publishing

Use [conventional commit format](https://www.conventionalcommits.org/en/v1.0.0/#summary) in PR titles to trigger releases via `release-please` task in drone pipeline.

- `chore:` & `build:` commits don't trigger releases (used for changes like updating config files or documentation)
- `fix:` bumps the patch version
- `feat:` bumps the minor version
- `feat!:` bumps the major version

# Formatting / Linting

```sh
$ cargo fmt    --manifest-path decider-py/test/Cargo.toml
$ cargo clippy --manifest-path decider-py/test/Cargo.toml
```
