Skip to content

Commit

Permalink
Add Config.duration (#2407)
Browse files Browse the repository at this point in the history
Co-authored-by: Tim <hello@timsmart.co>
  • Loading branch information
thewilkybarkid and tim-smart committed Mar 31, 2024
1 parent 1e0087d commit 196c5b0
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 2 deletions.
15 changes: 15 additions & 0 deletions .changeset/curvy-colts-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"effect": patch
---

Add Config.duration

This can be used to parse Duration's from environment variables:

```ts
import { Config, Effect } from "effect"

Config.duration("CACHE_TTL").pipe(
Effect.andThen((duration) => ...)
)
```
9 changes: 9 additions & 0 deletions packages/effect/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
import type * as Chunk from "./Chunk.js"
import type * as ConfigError from "./ConfigError.js"
import type * as Duration from "./Duration.js"
import type * as Effect from "./Effect.js"
import type * as Either from "./Either.js"
import type { LazyArg } from "./Function.js"
Expand Down Expand Up @@ -180,6 +181,14 @@ export const literal: <Literals extends ReadonlyArray<LiteralValue>>(...literals
*/
export const logLevel: (name?: string) => Config<LogLevel.LogLevel> = internal.logLevel

/**
* Constructs a config for a duration value.
*
* @since 2.5.0
* @category constructors
*/
export const duration: (name?: string) => Config<Duration.Duration> = internal.duration

/**
* This function returns `true` if the specified value is an `Config` value,
* `false` otherwise.
Expand Down
9 changes: 7 additions & 2 deletions packages/effect/src/Duration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import * as Option from "./Option.js"
import * as order from "./Order.js"
import type { Pipeable } from "./Pipeable.js"
import { pipeArguments } from "./Pipeable.js"
import { hasProperty, isBigInt, isNumber } from "./Predicate.js"
import { hasProperty, isBigInt, isNumber, isString } from "./Predicate.js"

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

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

/**
* @since 2.5.0
*/
export const decodeUnknown: (u: unknown) => Option.Option<Duration> = Option.liftThrowable(decode) as any

const zeroValue: DurationValue = { _tag: "Millis", millis: 0 }
const infinityValue: DurationValue = { _tag: "Infinity" }

Expand Down
10 changes: 10 additions & 0 deletions packages/effect/src/internal/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as Chunk from "../Chunk.js"
import type * as Config from "../Config.js"
import * as ConfigError from "../ConfigError.js"
import * as Duration from "../Duration.js"
import * as Either from "../Either.js"
import type { LazyArg } from "../Function.js"
import { constTrue, dual, pipe } from "../Function.js"
Expand Down Expand Up @@ -288,6 +289,15 @@ export const logLevel = (name?: string): Config.Config<LogLevel.LogLevel> => {
return name === undefined ? config : nested(config, name)
}

/** @internal */
export const duration = (name?: string): Config.Config<Duration.Duration> => {
const config = mapOrFail(string(), (value) => {
const duration = Duration.decodeUnknown(value)
return Either.fromOption(duration, () => configError.InvalidData([], `Expected a duration but received ${value}`))
})
return name === undefined ? config : nested(config, name)
}

/** @internal */
export const map = dual<
<A, B>(f: (a: A) => B) => (self: Config.Config<A>) => Config.Config<B>,
Expand Down
21 changes: 21 additions & 0 deletions packages/effect/test/Config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as Chunk from "effect/Chunk"
import * as Config from "effect/Config"
import * as ConfigError from "effect/ConfigError"
import * as ConfigProvider from "effect/ConfigProvider"
import * as Duration from "effect/Duration"
import * as Effect from "effect/Effect"
import * as Equal from "effect/Equal"
import * as Exit from "effect/Exit"
Expand Down Expand Up @@ -196,6 +197,26 @@ describe("Config", () => {
})
})

describe("duration", () => {
it("name = undefined", () => {
const config = Config.duration()
assertSuccess(config, [["", "10 seconds"]], Duration.decode("10 seconds"))

assertFailure(config, [["", "-"]], ConfigError.InvalidData([], "Expected a duration but received -"))
})

it("name != undefined", () => {
const config = Config.duration("DURATION")
assertSuccess(config, [["DURATION", "10 seconds"]], Duration.decode("10 seconds"))

assertFailure(
config,
[["DURATION", "-"]],
ConfigError.InvalidData(["DURATION"], "Expected a duration but received -")
)
})
})

describe("validate", () => {
it("should preserve the original path", () => {
const flat = Config.number("NUMBER").pipe(
Expand Down
38 changes: 38 additions & 0 deletions packages/effect/test/Duration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,44 @@ describe("Duration", () => {
expect(Duration.decode([-500, 123456789])).toEqual(Duration.zero)

expect(() => Duration.decode("1.5 secs" as any)).toThrowError(new Error("Invalid duration input"))
expect(() => Duration.decode(true as any)).toThrowError(new Error("Invalid duration input"))
expect(() => Duration.decode({} as any)).toThrowError(new Error("Invalid duration input"))
})

it("decodeUnknown", () => {
const millis100 = Duration.millis(100)
expect(Duration.decodeUnknown(millis100)).toEqual(Option.some(millis100))

expect(Duration.decodeUnknown(100)).toEqual(Option.some(millis100))

expect(Duration.decodeUnknown(10n)).toEqual(Option.some(Duration.nanos(10n)))

expect(Duration.decodeUnknown("1 nano")).toEqual(Option.some(Duration.nanos(1n)))
expect(Duration.decodeUnknown("10 nanos")).toEqual(Option.some(Duration.nanos(10n)))
expect(Duration.decodeUnknown("1 micro")).toEqual(Option.some(Duration.micros(1n)))
expect(Duration.decodeUnknown("10 micros")).toEqual(Option.some(Duration.micros(10n)))
expect(Duration.decodeUnknown("1 milli")).toEqual(Option.some(Duration.millis(1)))
expect(Duration.decodeUnknown("10 millis")).toEqual(Option.some(Duration.millis(10)))
expect(Duration.decodeUnknown("1 second")).toEqual(Option.some(Duration.seconds(1)))
expect(Duration.decodeUnknown("10 seconds")).toEqual(Option.some(Duration.seconds(10)))
expect(Duration.decodeUnknown("1 minute")).toEqual(Option.some(Duration.minutes(1)))
expect(Duration.decodeUnknown("10 minutes")).toEqual(Option.some(Duration.minutes(10)))
expect(Duration.decodeUnknown("1 hour")).toEqual(Option.some(Duration.hours(1)))
expect(Duration.decodeUnknown("10 hours")).toEqual(Option.some(Duration.hours(10)))
expect(Duration.decodeUnknown("1 day")).toEqual(Option.some(Duration.days(1)))
expect(Duration.decodeUnknown("10 days")).toEqual(Option.some(Duration.days(10)))
expect(Duration.decodeUnknown("1 week")).toEqual(Option.some(Duration.weeks(1)))
expect(Duration.decodeUnknown("10 weeks")).toEqual(Option.some(Duration.weeks(10)))

expect(Duration.decodeUnknown("1.5 seconds")).toEqual(Option.some(Duration.seconds(1.5)))
expect(Duration.decodeUnknown("-1.5 seconds")).toEqual(Option.some(Duration.zero))

expect(Duration.decodeUnknown([500, 123456789])).toEqual(Option.some(Duration.nanos(500123456789n)))
expect(Duration.decodeUnknown([-500, 123456789])).toEqual(Option.some(Duration.zero))

expect(Duration.decodeUnknown("1.5 secs")).toEqual(Option.none())
expect(Duration.decodeUnknown(true)).toEqual(Option.none())
expect(Duration.decodeUnknown({})).toEqual(Option.none())
})

it("Order", () => {
Expand Down

0 comments on commit 196c5b0

Please sign in to comment.