Skip to content

Commit 63d9867

Browse files
committed
JSON.parse coercion
1 parent 76e946d commit 63d9867

File tree

4 files changed

+140
-34
lines changed

4 files changed

+140
-34
lines changed

README.md

+7-7
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,13 @@ Gets an environment variable from `envSafe` applying coercion and parsing logic
8383

8484
#### Possible options
8585

86-
| Name | Type | Description |
87-
| ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
88-
| `name` | `string` | Name of the environment variable |
89-
| `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. |
90-
| `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. If you need to set fallback value for all environments, you can use `S.Option.getOr` on schema. |
91-
| `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")`. |
92-
| `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`. |
86+
| Name | Type | Description |
87+
| ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
88+
| `name` | `string` | Name of the environment variable |
89+
| `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. |
90+
| `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. If you need to set fallback value for all environments, you can use `S.Option.getOr` on schema. |
91+
| `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")`. |
92+
| `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`. |
9393

9494
### **`EnvSafe.close`**
9595

__tests__/EnvSafe_json_test.res

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
open Ava
2+
3+
test(`Uses JSON parsing with object schema`, t => {
4+
let envSafe = EnvSafe.make(
5+
~env=Obj.magic({
6+
"OBJ_ENV": `{"foo":true}`,
7+
}),
8+
)
9+
10+
t->Assert.deepEqual(
11+
envSafe->EnvSafe.get("OBJ_ENV", S.object(s => s.field("foo", S.bool))),
12+
true,
13+
(),
14+
)
15+
t->Assert.notThrows(() => {
16+
envSafe->EnvSafe.close
17+
}, ())
18+
})
19+
20+
test(`Uses JSON parsing with array schema`, t => {
21+
let envSafe = EnvSafe.make(
22+
~env=Obj.magic({
23+
"ENV": `[1, 2]`,
24+
}),
25+
)
26+
27+
t->Assert.deepEqual(envSafe->EnvSafe.get("ENV", S.array(S.int)), [1, 2], ())
28+
t->Assert.notThrows(() => {
29+
envSafe->EnvSafe.close
30+
}, ())
31+
})
32+
33+
test(`Doens't use JSON parsing with unknown schema`, t => {
34+
let envSafe = EnvSafe.make(
35+
~env=Obj.magic({
36+
"ENV": `[1, 2]`,
37+
}),
38+
)
39+
40+
t->Assert.deepEqual(envSafe->EnvSafe.get("ENV", S.unknown), `[1, 2]`->Obj.magic, ())
41+
t->Assert.notThrows(() => {
42+
envSafe->EnvSafe.close
43+
}, ())
44+
})
45+
46+
test(`Doens't use JSON parsing with never schema`, t => {
47+
let envSafe = EnvSafe.make(
48+
~env=Obj.magic({
49+
"ENV": `[1, 2]`,
50+
}),
51+
)
52+
53+
t->Assert.deepEqual(envSafe->EnvSafe.get("ENV", S.never), %raw(`undefined`), ())
54+
t->Assert.throws(
55+
() => {
56+
envSafe->EnvSafe.close
57+
},
58+
~expectations={
59+
message: `========================================
60+
❌ Invalid environment variables:
61+
ENV: Failed parsing at root. Reason: Expected Never, received "[1, 2]"
62+
========================================`,
63+
},
64+
(),
65+
)
66+
})

src/EnvSafe.bs.js

+40-19
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EnvSafe.res

+27-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
@@uncurried
22

3+
%%private(external magic: 'a => 'b = "%identity")
4+
35
module Stdlib = {
46
module Dict = {
57
@get_index external get: (Js.Dict.t<'a>, string) => option<'a> = ""
@@ -32,7 +34,7 @@ module Stdlib = {
3234
@new
3335
external makeTypeError: string => error = "TypeError"
3436

35-
let raiseError = (error: error): 'a => error->Obj.magic->raise
37+
let raiseError = (error: error): 'a => error->magic->raise
3638
}
3739
}
3840

@@ -57,6 +59,8 @@ module Env = {
5759
default: Js.Dict.t<string> = "process.env"
5860
}
5961

62+
// TODO: When can't coerce, default to json parsing
63+
6064
let mixinIssue = (envSafe, issue) => {
6165
switch issue.error {
6266
| {code: InvalidType({received})}
@@ -132,15 +136,15 @@ let prepareSchema = (~schema, ~allowEmpty) => {
132136
| Literal(Boolean(_))
133137
| Bool => {
134138
parser: unknown => {
135-
switch unknown->Obj.magic {
139+
switch unknown->magic {
136140
| "true"
137141
| "t"
138142
| "1" => true
139143
| "false"
140144
| "f"
141145
| "0" => false
142-
| _ => unknown->Obj.magic
143-
}->Obj.magic
146+
| _ => unknown->magic
147+
}->magic
144148
},
145149
}
146150

@@ -162,13 +166,28 @@ let prepareSchema = (~schema, ~allowEmpty) => {
162166
}
163167
| String if allowEmpty === false => {
164168
parser: unknown => {
165-
switch unknown->Obj.magic {
166-
| "" => Js.undefined->Obj.magic
167-
| _ => unknown->Obj.magic
169+
switch unknown->magic {
170+
| "" => Js.undefined->magic
171+
| _ => unknown->magic
172+
}
173+
},
174+
}
175+
| String
176+
| Literal(String(_))
177+
| JSON
178+
| Union(_)
179+
| Unknown
180+
| Never => {}
181+
| _ => {
182+
parser: unknown => {
183+
if unknown->Js.typeof === "string" {
184+
let string = unknown->(magic: unknown => string)
185+
string->Js.Json.parseExn->magic
186+
} else {
187+
unknown
168188
}
169189
},
170190
}
171-
| _ => {}
172191
}
173192
})
174193
}

0 commit comments

Comments
 (0)