Skip to content

Commit

Permalink
v5.0.0 🚀 - ReScript Schema V9 & BigInt support
Browse files Browse the repository at this point in the history
  • Loading branch information
DZakh committed Dec 22, 2024
1 parent aeac501 commit decfe04
Show file tree
Hide file tree
Showing 9 changed files with 1,922 additions and 1,009 deletions.
58 changes: 28 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,7 @@ Heavily inspired by the great project [envsafe](https://github.com/KATT/envsafe)
- Built for node.js **and** the browser
- **Composable** parsers with **[rescript-schema](https://github.com/DZakh/rescript-schema)**

## How to use

### Install

```sh
npm install rescript-envsafe rescript-schema
```

Then add `rescript-envsafe` and `rescript-schema` to `bs-dependencies` in your `rescript.json`:

```diff
{
...
+ "bs-dependencies": ["rescript-envsafe", "rescript-schema"],
+ "bsc-flags": ["-open RescriptSchema"],
}
```

### Basic usage
## Basic usage

```rescript
%%private(let envSafe = EnvSafe.make())
Expand All @@ -52,15 +34,31 @@ let nodeEnv = envSafe->EnvSafe.get(
]),
~devFallback=#development,
)
let port = envSafe->EnvSafe.get("PORT", S.int->S.Int.port, ~fallback=3000)
let apiUrl = envSafe->EnvSafe.get("API_URL", S.string->S.String.url, ~devFallback="https://example.com/graphql")
let port = envSafe->EnvSafe.get("PORT", S.int->S.port, ~fallback=3000)
let apiUrl = envSafe->EnvSafe.get("API_URL", S.string->S.url, ~devFallback="https://example.com/graphql")
let auth0ClientId = envSafe->EnvSafe.get("AUTH0_CLIENT_ID", S.string)
let auth0Domain = envSafe->EnvSafe.get("AUTH0_DOMAIN", S.string)
// 🧠 If you forget to close `envSafe` then invalid vars end up being `undefined` leading to an expected runtime error.
envSafe->EnvSafe.close
```

## Install

```sh
npm install rescript-envsafe rescript-schema
```

Then add `rescript-envsafe` and `rescript-schema` to `bs-dependencies` in your `rescript.json`:

```diff
{
...
+ "bs-dependencies": ["rescript-envsafe", "rescript-schema"],
+ "bsc-flags": ["-open RescriptSchema"],
}
```

## API Reference

### **`EnvSafe.make`**
Expand All @@ -78,21 +76,21 @@ Creates `envSafe` to start working with environment variables. By default it use
`(EnvSafe.t, string, S.t<'value>, ~allowEmpty: bool=?, ~fallback: 'value=?, ~devFallback: 'value=?, ~input: option<string>=?) => 'value`

```rescript
let port = envSafe->EnvSafe.get("PORT", S.int->S.Int.port, ~fallback=3000)
let port = envSafe->EnvSafe.get("PORT", S.int->S.port, ~fallback=3000)
```

Gets an environment variable from `envSafe` applying coercion and parsing logic of `schema`.

#### Possible options

| Name | Type | Description |
| ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | `string` | Name of the environment variable |
| `schema` | `S.t<'value>` | A schema created with **[rescript-schema](https://github.com/DZakh/rescript-schema)**. It's used for coercion and parsing. For bool schemas coerces `"0", "1", "true", "false", "t", "f"` to boolean values. For int and float schemas coerces string to number. For other non-string schemas the value is coerced using `JSON.parse` before being validated. |
| `fallback` | `'value=?` | A fallback value when the environment variable is missing. |
| `devFallback` | `'value=?` | A fallback value to use only when `NODE_ENV` is not `production`. This is handy for env vars that are required for production environments, but optional for development and testing. |
| `input` | `string=?` | As some environments don't allow you to dynamically read env vars, we can manually put it in as well. Example: `input=%raw("process.env.NEXT_PUBLIC_API_URL")`. |
| `allowEmpty` | `bool=false` | Default behavior is `false` which treats empty strings as the value is missing. if explicit empty strings are OK, pass in `true`. |
| Name | Type | Description |
| ------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | `string` | Name of the environment variable |
| `schema` | `S.t<'value>` | A schema created with **[rescript-schema](https://github.com/DZakh/rescript-schema)**. It's used for coercion and parsing. For bool schemas coerces `"0", "1", "true", "false", "t", "f"` to boolean values. For int, float and bigint schemas coerces string to number. For other non-string schemas the value is coerced using `JSON.parse` before being validated. |
| `fallback` | `'value=?` | A fallback value when the environment variable is missing. |
| `devFallback` | `'value=?` | A fallback value to use only when `NODE_ENV` is not `production`. This is handy for env vars that are required for production environments, but optional for development and testing. |
| `input` | `string=?` | As some environments don't allow you to dynamically read env vars, we can manually put it in as well. Example: `input=%raw("process.env.NEXT_PUBLIC_API_URL")`. |
| `allowEmpty` | `bool=false` | Default behavior is `false` which treats empty strings as the value is missing. if explicit empty strings are OK, pass in `true`. |

### **`EnvSafe.close`**

Expand Down
2 changes: 1 addition & 1 deletion __tests__/EnvSafe_bool_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ test(`Fails to get Bool value when the env is "2"`, t => {
name: "TypeError",
message: `========================================
❌ Invalid environment variables:
BOOL_ENV: Failed parsing at root. Reason: Expected Bool, received "2"
BOOL_ENV: Failed parsing at root. Reason: Expected boolean, received "2"
========================================`,
},
)
Expand Down
6 changes: 3 additions & 3 deletions __tests__/EnvSafe_json_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ test(`Uses JSON parsing with JSON schema`, t => {
})
})

test(`Doens't use JSON parsing with never schema`, t => {
test(`Doesn't use JSON parsing with never schema`, t => {
let envSafe = EnvSafe.make(
~env=Obj.magic({
"ENV": `[1, 2]`,
Expand All @@ -67,7 +67,7 @@ test(`Doens't use JSON parsing with never schema`, t => {
~expectations={
message: `========================================
❌ Invalid environment variables:
ENV: Failed parsing at root. Reason: Expected Never, received "[1, 2]"
ENV: Failed parsing at root. Reason: Expected never, received "[1, 2]"
========================================`,
},
)
Expand All @@ -88,7 +88,7 @@ test(`Fails with invalid json string`, t => {
~expectations={
message: `========================================
❌ Invalid environment variables:
ENV: Failed parsing at root. Reason: Expected Array(Int), received "[1, 2],"
ENV: Failed parsing at root. Reason: Expected array<int32>, received "[1, 2],"
========================================`,
},
)
Expand Down
28 changes: 27 additions & 1 deletion __tests__/EnvSafe_number_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,32 @@ test(`Successfully get Literal Float value when the env is "1"`, t => {
})
})

test(`Successfully get Literal BigInt value when the env is "1"`, t => {
let envSafe = EnvSafe.make(
~env=Obj.magic({
"INT_ENV": "1",
}),
)

t->Assert.is(envSafe->EnvSafe.get("INT_ENV", S.literal(1n)), 1n)
t->Assert.notThrows(() => {
envSafe->EnvSafe.close
})
})

test(`Successfully get BigInt value when the env is "1"`, t => {
let envSafe = EnvSafe.make(
~env=Obj.magic({
"INT_ENV": "1",
}),
)

t->Assert.is(envSafe->EnvSafe.get("INT_ENV", S.bigint), 1n)
t->Assert.notThrows(() => {
envSafe->EnvSafe.close
})
})

test(`Fails to get invalid number`, t => {
let envSafe = EnvSafe.make(
~env=Obj.magic({
Expand All @@ -68,7 +94,7 @@ test(`Fails to get invalid number`, t => {
name: "TypeError",
message: `========================================
❌ Invalid environment variables:
INT_ENV: Failed parsing at root. Reason: Expected Int, received "1_000"
INT_ENV: Failed parsing at root. Reason: Expected int32, received "1_000"
========================================`,
},
)
Expand Down
14 changes: 7 additions & 7 deletions __tests__/EnvSafe_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ test("Uses devFallback value when env is missing", t => {
t->Assert.is(
envSafe->EnvSafe.get(
"MISSING_ENV",
S.literal("invalid")->S.variant(_ => #polymorphicToTestFunctionType2),
S.literal("invalid")->S.to(_ => #polymorphicToTestFunctionType2),
~devFallback=#polymorphicToTestFunctionType,
),
#polymorphicToTestFunctionType,
Expand All @@ -160,7 +160,7 @@ test("Uses fallback value when env is missing", t => {
t->Assert.is(
envSafe->EnvSafe.get(
"MISSING_ENV",
S.literal("invalid")->S.variant(_ => #polymorphicToTestFunctionType2),
S.literal("invalid")->S.to(_ => #polymorphicToTestFunctionType2),
~fallback=#polymorphicToTestFunctionType,
),
#polymorphicToTestFunctionType,
Expand Down Expand Up @@ -226,7 +226,7 @@ test("Doesn't use devFallback value when NODE_ENV is production", t => {
t->Assert.is(
envSafe->EnvSafe.get(
"MISSING_ENV",
S.literal("invalid")->S.variant(_ => #polymorphicToTestFunctionType2),
S.literal("invalid")->S.to(_ => #polymorphicToTestFunctionType2),
~devFallback=#polymorphicToTestFunctionType,
),
%raw(`undefined`),
Expand Down Expand Up @@ -305,7 +305,7 @@ test("Closes with 1 valid, 3 missing and 2 invalid environment variables", t =>
name: "TypeError",
message: `========================================
❌ Invalid environment variables:
BOOL_ENV1: Failed parsing at root. Reason: Expected Int, received "true"
BOOL_ENV1: Failed parsing at root. Reason: Expected int32, received "true"
BOOL_ENV2: Failed parsing at root. Reason: Expected true, received false
💨 Missing environment variables:
MISSING_ENV1: Missing value
Expand Down Expand Up @@ -343,9 +343,9 @@ test(`Doesn't show input value when it's missing for invalid env`, t => {

test("Applies preprocessor logic for union schemas separately", t => {
let schema = S.union([
S.bool->S.variant(bool => #Bool(bool)),
S.string->S.variant(string => #String(string)),
S.union([S.int->S.variant(int => #Int(int)), S.string->S.variant(string => #String(string))]),
S.bool->S.to(bool => #Bool(bool)),
S.string->S.to(string => #String(string)),
S.union([S.int->S.to(int => #Int(int)), S.string->S.to(string => #String(string))]),
])

let envSafe = EnvSafe.make(~env=Obj.magic(Js.Dict.empty()))
Expand Down
Loading

0 comments on commit decfe04

Please sign in to comment.