Skip to content

Commit d7688c0

Browse files
thewilkybarkidtim-smart
andauthoredMar 26, 2024··
Add Config.duration (#2407)
Co-authored-by: Tim <hello@timsmart.co>
1 parent c34eb3e commit d7688c0

File tree

6 files changed

+100
-2
lines changed

6 files changed

+100
-2
lines changed
 

‎.changeset/curvy-colts-clap.md

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"effect": patch
3+
---
4+
5+
Add Config.duration
6+
7+
This can be used to parse Duration's from environment variables:
8+
9+
```ts
10+
import { Config, Effect } from "effect"
11+
12+
Config.duration("CACHE_TTL").pipe(
13+
Effect.andThen((duration) => ...)
14+
)
15+
```

‎packages/effect/src/Config.ts

+9
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44
import type * as Chunk from "./Chunk.js"
55
import type * as ConfigError from "./ConfigError.js"
6+
import type * as Duration from "./Duration.js"
67
import type * as Effect from "./Effect.js"
78
import type * as Either from "./Either.js"
89
import type { LazyArg } from "./Function.js"
@@ -180,6 +181,14 @@ export const literal: <Literals extends ReadonlyArray<LiteralValue>>(...literals
180181
*/
181182
export const logLevel: (name?: string) => Config<LogLevel.LogLevel> = internal.logLevel
182183

184+
/**
185+
* Constructs a config for a duration value.
186+
*
187+
* @since 2.5.0
188+
* @category constructors
189+
*/
190+
export const duration: (name?: string) => Config<Duration.Duration> = internal.duration
191+
183192
/**
184193
* This function returns `true` if the specified value is an `Config` value,
185194
* `false` otherwise.

‎packages/effect/src/Duration.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import * as Option from "./Option.js"
1111
import * as order from "./Order.js"
1212
import type { Pipeable } from "./Pipeable.js"
1313
import { pipeArguments } from "./Pipeable.js"
14-
import { hasProperty, isBigInt, isNumber } from "./Predicate.js"
14+
import { hasProperty, isBigInt, isNumber, isString } from "./Predicate.js"
1515

1616
const TypeId: unique symbol = Symbol.for("effect/Duration")
1717

@@ -94,7 +94,7 @@ export const decode = (input: DurationInput): Duration => {
9494
if (input.length === 2 && isNumber(input[0]) && isNumber(input[1])) {
9595
return nanos(BigInt(input[0]) * bigint1e9 + BigInt(input[1]))
9696
}
97-
} else {
97+
} else if (isString(input)) {
9898
DURATION_REGEX.lastIndex = 0 // Reset the lastIndex before each use
9999
const match = DURATION_REGEX.exec(input)
100100
if (match) {
@@ -131,6 +131,11 @@ export const decode = (input: DurationInput): Duration => {
131131
throw new Error("Invalid duration input")
132132
}
133133

134+
/**
135+
* @since 2.5.0
136+
*/
137+
export const decodeUnknown: (u: unknown) => Option.Option<Duration> = Option.liftThrowable(decode) as any
138+
134139
const zeroValue: DurationValue = { _tag: "Millis", millis: 0 }
135140
const infinityValue: DurationValue = { _tag: "Infinity" }
136141

‎packages/effect/src/internal/config.ts

+10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as Chunk from "../Chunk.js"
22
import type * as Config from "../Config.js"
33
import * as ConfigError from "../ConfigError.js"
4+
import * as Duration from "../Duration.js"
45
import * as Either from "../Either.js"
56
import type { LazyArg } from "../Function.js"
67
import { constTrue, dual, pipe } from "../Function.js"
@@ -288,6 +289,15 @@ export const logLevel = (name?: string): Config.Config<LogLevel.LogLevel> => {
288289
return name === undefined ? config : nested(config, name)
289290
}
290291

292+
/** @internal */
293+
export const duration = (name?: string): Config.Config<Duration.Duration> => {
294+
const config = mapOrFail(string(), (value) => {
295+
const duration = Duration.decodeUnknown(value)
296+
return Either.fromOption(duration, () => configError.InvalidData([], `Expected a duration but received ${value}`))
297+
})
298+
return name === undefined ? config : nested(config, name)
299+
}
300+
291301
/** @internal */
292302
export const map = dual<
293303
<A, B>(f: (a: A) => B) => (self: Config.Config<A>) => Config.Config<B>,

‎packages/effect/test/Config.test.ts

+21
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as Chunk from "effect/Chunk"
22
import * as Config from "effect/Config"
33
import * as ConfigError from "effect/ConfigError"
44
import * as ConfigProvider from "effect/ConfigProvider"
5+
import * as Duration from "effect/Duration"
56
import * as Effect from "effect/Effect"
67
import * as Equal from "effect/Equal"
78
import * as Exit from "effect/Exit"
@@ -196,6 +197,26 @@ describe("Config", () => {
196197
})
197198
})
198199

200+
describe("duration", () => {
201+
it("name = undefined", () => {
202+
const config = Config.duration()
203+
assertSuccess(config, [["", "10 seconds"]], Duration.decode("10 seconds"))
204+
205+
assertFailure(config, [["", "-"]], ConfigError.InvalidData([], "Expected a duration but received -"))
206+
})
207+
208+
it("name != undefined", () => {
209+
const config = Config.duration("DURATION")
210+
assertSuccess(config, [["DURATION", "10 seconds"]], Duration.decode("10 seconds"))
211+
212+
assertFailure(
213+
config,
214+
[["DURATION", "-"]],
215+
ConfigError.InvalidData(["DURATION"], "Expected a duration but received -")
216+
)
217+
})
218+
})
219+
199220
describe("validate", () => {
200221
it("should preserve the original path", () => {
201222
const flat = Config.number("NUMBER").pipe(

‎packages/effect/test/Duration.test.ts

+38
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,44 @@ describe("Duration", () => {
4242
expect(Duration.decode([-500, 123456789])).toEqual(Duration.zero)
4343

4444
expect(() => Duration.decode("1.5 secs" as any)).toThrowError(new Error("Invalid duration input"))
45+
expect(() => Duration.decode(true as any)).toThrowError(new Error("Invalid duration input"))
46+
expect(() => Duration.decode({} as any)).toThrowError(new Error("Invalid duration input"))
47+
})
48+
49+
it("decodeUnknown", () => {
50+
const millis100 = Duration.millis(100)
51+
expect(Duration.decodeUnknown(millis100)).toEqual(Option.some(millis100))
52+
53+
expect(Duration.decodeUnknown(100)).toEqual(Option.some(millis100))
54+
55+
expect(Duration.decodeUnknown(10n)).toEqual(Option.some(Duration.nanos(10n)))
56+
57+
expect(Duration.decodeUnknown("1 nano")).toEqual(Option.some(Duration.nanos(1n)))
58+
expect(Duration.decodeUnknown("10 nanos")).toEqual(Option.some(Duration.nanos(10n)))
59+
expect(Duration.decodeUnknown("1 micro")).toEqual(Option.some(Duration.micros(1n)))
60+
expect(Duration.decodeUnknown("10 micros")).toEqual(Option.some(Duration.micros(10n)))
61+
expect(Duration.decodeUnknown("1 milli")).toEqual(Option.some(Duration.millis(1)))
62+
expect(Duration.decodeUnknown("10 millis")).toEqual(Option.some(Duration.millis(10)))
63+
expect(Duration.decodeUnknown("1 second")).toEqual(Option.some(Duration.seconds(1)))
64+
expect(Duration.decodeUnknown("10 seconds")).toEqual(Option.some(Duration.seconds(10)))
65+
expect(Duration.decodeUnknown("1 minute")).toEqual(Option.some(Duration.minutes(1)))
66+
expect(Duration.decodeUnknown("10 minutes")).toEqual(Option.some(Duration.minutes(10)))
67+
expect(Duration.decodeUnknown("1 hour")).toEqual(Option.some(Duration.hours(1)))
68+
expect(Duration.decodeUnknown("10 hours")).toEqual(Option.some(Duration.hours(10)))
69+
expect(Duration.decodeUnknown("1 day")).toEqual(Option.some(Duration.days(1)))
70+
expect(Duration.decodeUnknown("10 days")).toEqual(Option.some(Duration.days(10)))
71+
expect(Duration.decodeUnknown("1 week")).toEqual(Option.some(Duration.weeks(1)))
72+
expect(Duration.decodeUnknown("10 weeks")).toEqual(Option.some(Duration.weeks(10)))
73+
74+
expect(Duration.decodeUnknown("1.5 seconds")).toEqual(Option.some(Duration.seconds(1.5)))
75+
expect(Duration.decodeUnknown("-1.5 seconds")).toEqual(Option.some(Duration.zero))
76+
77+
expect(Duration.decodeUnknown([500, 123456789])).toEqual(Option.some(Duration.nanos(500123456789n)))
78+
expect(Duration.decodeUnknown([-500, 123456789])).toEqual(Option.some(Duration.zero))
79+
80+
expect(Duration.decodeUnknown("1.5 secs")).toEqual(Option.none())
81+
expect(Duration.decodeUnknown(true)).toEqual(Option.none())
82+
expect(Duration.decodeUnknown({})).toEqual(Option.none())
4583
})
4684

4785
it("Order", () => {

0 commit comments

Comments
 (0)
Please sign in to comment.