Skip to content

Commit

Permalink
Add HRTime schema for backward compatibility with Duration (#4327)
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti authored Jan 22, 2025
1 parent 602939a commit 55147c5
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 28 deletions.
2 changes: 1 addition & 1 deletion packages/effect/dtslint/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1763,7 +1763,7 @@ S.BigDecimalFromNumber
// Duration
// ---------------------------------------------

// $ExpectType Schema<Duration, DurationEncoded, never>
// $ExpectType Schema<Duration, DurationEncoded | readonly [seconds: number, nanos: number], never>
S.asSchema(S.Duration)

// $ExpectType typeof Duration
Expand Down
36 changes: 28 additions & 8 deletions packages/effect/src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5750,26 +5750,46 @@ const DurationValue: Schema<duration_.DurationValue, DurationEncoded> = Union(
description: "an JSON-compatible tagged union to be decoded into a Duration"
})

const FiniteHRTime = Tuple(
element(NonNegativeInt).annotations({ title: "seconds" }),
element(NonNegativeInt).annotations({ title: "nanos" })
).annotations({ identifier: "FiniteHRTime" })

const InfiniteHRTime = Tuple(Literal(-1), Literal(0)).annotations({ identifier: "InfiniteHRTime" })

const HRTime: Schema<readonly [seconds: number, nanos: number]> = Union(FiniteHRTime, InfiniteHRTime).annotations({
identifier: "HRTime",
description: "a tuple of seconds and nanos to be decoded into a Duration"
})

const isDurationValue = (u: duration_.DurationValue | typeof HRTime.Type): u is duration_.DurationValue =>
typeof u === "object"

/**
* A schema that converts a JSON-compatible tagged union into a `Duration`.
*
* @category Duration transformations
* @since 3.10.0
*/
export class Duration extends transform(
DurationValue,
// TODO: remove HRTime in next major version
Union(DurationValue, HRTime),
DurationFromSelf,
{
strict: true,
decode: (input) => {
switch (input._tag) {
case "Millis":
return duration_.millis(input.millis)
case "Nanos":
return duration_.nanos(input.nanos)
case "Infinity":
return duration_.infinity
if (isDurationValue(input)) {
switch (input._tag) {
case "Millis":
return duration_.millis(input.millis)
case "Nanos":
return duration_.nanos(input.nanos)
case "Infinity":
return duration_.infinity
}
}
const [seconds, nanos] = input
return seconds === -1 ? duration_.infinity : duration_.nanos(BigInt(seconds) * BigInt(1e9) + BigInt(nanos))
},
encode: (duration) => {
switch (duration.value._tag) {
Expand Down
131 changes: 112 additions & 19 deletions packages/effect/test/Schema/Schema/Duration/Duration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,46 +20,139 @@ describe("Duration", () => {
null,
`Duration
└─ Encoded side transformation failure
└─ Expected DurationValue, actual null`
└─ DurationValue | HRTime
├─ Expected DurationValue, actual null
└─ HRTime
├─ Expected InfiniteHRTime, actual null
└─ Expected FiniteHRTime, actual null`
)

await Util.assertions.decoding.fail(
schema,
{},
`Duration
└─ Encoded side transformation failure
└─ DurationValue
└─ { readonly _tag: "Millis" | "Nanos" | "Infinity" }
└─ ["_tag"]
└─ is missing`
└─ DurationValue | HRTime
├─ DurationValue
│ └─ { readonly _tag: "Millis" | "Nanos" | "Infinity" }
│ └─ ["_tag"]
│ └─ is missing
└─ HRTime
├─ InfiniteHRTime
│ └─ ["0"]
│ └─ is missing
└─ Expected FiniteHRTime, actual {}`
)

await Util.assertions.decoding.fail(
schema,
{ _tag: "Millis", millis: -1 },
`Duration
└─ Encoded side transformation failure
└─ DurationValue
└─ { readonly _tag: "Millis"; readonly millis: NonNegativeInt }
└─ ["millis"]
└─ NonNegativeInt
└─ From side refinement failure
└─ NonNegative
└─ Predicate refinement failure
└─ Expected a non-negative number, actual -1`
└─ DurationValue | HRTime
├─ DurationValue
│ └─ { readonly _tag: "Millis"; readonly millis: NonNegativeInt }
│ └─ ["millis"]
│ └─ NonNegativeInt
│ └─ From side refinement failure
│ └─ NonNegative
│ └─ Predicate refinement failure
│ └─ Expected a non-negative number, actual -1
└─ HRTime
├─ InfiniteHRTime
│ └─ ["0"]
│ └─ is missing
└─ Expected FiniteHRTime, actual {"_tag":"Millis","millis":-1}`
)

await Util.assertions.decoding.fail(
schema,
{ _tag: "Nanos", nanos: null },
`Duration
└─ Encoded side transformation failure
└─ DurationValue
└─ { readonly _tag: "Nanos"; readonly nanos: BigInt }
└─ ["nanos"]
└─ BigInt
└─ Encoded side transformation failure
└─ Expected string, actual null`
└─ DurationValue | HRTime
├─ DurationValue
│ └─ { readonly _tag: "Nanos"; readonly nanos: BigInt }
│ └─ ["nanos"]
│ └─ BigInt
│ └─ Encoded side transformation failure
│ └─ Expected string, actual null
└─ HRTime
├─ InfiniteHRTime
│ └─ ["0"]
│ └─ is missing
└─ Expected FiniteHRTime, actual {"_tag":"Nanos","nanos":null}`
)
})

it("HRTime backward compatible encoding", async () => {
await Util.assertions.decoding.succeed(schema, [-1, 0], Duration.infinity)
await Util.assertions.decoding.succeed(schema, [555, 123456789], Duration.nanos(555123456789n))
await Util.assertions.decoding.fail(
schema,
[-500, 0],
`Duration
└─ Encoded side transformation failure
└─ DurationValue | HRTime
├─ DurationValue
│ └─ { readonly _tag: "Millis" | "Nanos" | "Infinity" }
│ └─ ["_tag"]
│ └─ is missing
└─ HRTime
├─ InfiniteHRTime
│ └─ ["0"]
│ └─ Expected -1, actual -500
└─ FiniteHRTime
└─ [0]
└─ NonNegativeInt
└─ From side refinement failure
└─ NonNegative
└─ Predicate refinement failure
└─ Expected a non-negative number, actual -500`
)
await Util.assertions.decoding.fail(
schema,
[0, -123],
`Duration
└─ Encoded side transformation failure
└─ DurationValue | HRTime
├─ DurationValue
│ └─ { readonly _tag: "Millis" | "Nanos" | "Infinity" }
│ └─ ["_tag"]
│ └─ is missing
└─ HRTime
├─ InfiniteHRTime
│ └─ ["0"]
│ └─ Expected -1, actual 0
└─ FiniteHRTime
└─ [1]
└─ NonNegativeInt
└─ From side refinement failure
└─ NonNegative
└─ Predicate refinement failure
└─ Expected a non-negative number, actual -123`
)
await Util.assertions.decoding.fail(
schema,
123,
`Duration
└─ Encoded side transformation failure
└─ DurationValue | HRTime
├─ Expected DurationValue, actual 123
└─ HRTime
├─ Expected InfiniteHRTime, actual 123
└─ Expected FiniteHRTime, actual 123`
)
await Util.assertions.decoding.fail(
schema,
123n,
`Duration
└─ Encoded side transformation failure
└─ DurationValue | HRTime
├─ Expected DurationValue, actual 123n
└─ HRTime
├─ Expected InfiniteHRTime, actual 123n
└─ Expected FiniteHRTime, actual 123n`
)
})

Expand Down

0 comments on commit 55147c5

Please sign in to comment.