Skip to content

Commit

Permalink
Add ReadonlyMapFromRecord and MapFromRecord, closes #3119
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti committed Jun 30, 2024
1 parent 489d20a commit aa50781
Show file tree
Hide file tree
Showing 6 changed files with 244 additions and 0 deletions.
39 changes: 39 additions & 0 deletions .changeset/lemon-books-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
"@effect/schema": patch
---

Add `ReadonlyMapFromRecord` and `MapFromRecord`, closes #3119

- decoding
- `{ readonly [x: string]: VI }` -> `ReadonlyMap<KA, VA>`
- encoding
- `ReadonlyMap<KA, VA>` -> `{ readonly [x: string]: VI }`

```ts
import { Schema } from "@effect/schema"

const schema = Schema.ReadonlyMapFromRecord({
key: Schema.BigInt,
value: Schema.NumberFromString
})

const decode = Schema.decodeUnknownSync(schema)
const encode = Schema.encodeSync(schema)

console.log(
decode({
"1": "4",
"2": "5",
"3": "6"
})
) // Map(3) { 1n => 4, 2n => 5, 3n => 6 }
console.log(
encode(
new Map([
[1n, 4],
[2n, 5],
[3n, 6]
])
)
) // { '1': '4', '2': '5', '3': '6' }
```
36 changes: 36 additions & 0 deletions packages/schema/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7441,6 +7441,42 @@ console.log(
) // Map(3) { 'a' => '1', 'b' => '2', 'c' => '3' }
```

### ReadonlyMapFromRecord

- decoding
- `{ readonly [x: string]: VI }` -> `ReadonlyMap<KA, VA>`
- encoding
- `ReadonlyMap<KA, VA>` -> `{ readonly [x: string]: VI }`

```ts
import { Schema } from "@effect/schema"

const schema = Schema.ReadonlyMapFromRecord({
key: Schema.BigInt,
value: Schema.NumberFromString
})

const decode = Schema.decodeUnknownSync(schema)
const encode = Schema.encodeSync(schema)

console.log(
decode({
"1": "4",
"2": "5",
"3": "6"
})
) // Map(3) { 1n => 4, 2n => 5, 3n => 6 }
console.log(
encode(
new Map([
[1n, 4],
[2n, 5],
[3n, 6]
])
)
) // { '1': '4', '2': '5', '3': '6' }
```

## HashSet

### HashSet
Expand Down
27 changes: 27 additions & 0 deletions packages/schema/src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type * as Order from "effect/Order"
import type { Pipeable } from "effect/Pipeable"
import { pipeArguments } from "effect/Pipeable"
import * as Predicate from "effect/Predicate"
import * as record_ from "effect/Record"
import * as redacted_ from "effect/Redacted"
import * as Request from "effect/Request"
import * as sortedSet_ from "effect/SortedSet"
Expand Down Expand Up @@ -6249,6 +6250,32 @@ export {
map as Map
}

/**
* @category ReadonlyMap transformations
* @since 0.68.15
*/
export const ReadonlyMapFromRecord = <KA, KR, VA, VI, VR>({ key, value }: {
key: Schema<KA, string, KR>
value: Schema<VA, VI, VR>
}): Schema<ReadonlyMap<KA, VA>, { readonly [x: string]: VI }, KR | VR> =>
transform(Record(encodedBoundSchema(key), value), ReadonlyMapFromSelf({ key, value: typeSchema(value) }), {
decode: (record) => new Map(Object.entries(record)),
encode: record_.fromEntries
})

/**
* @category Map transformations
* @since 0.68.15
*/
export const MapFromRecord = <KA, KR, VA, VI, VR>({ key, value }: {
key: Schema<KA, string, KR>
value: Schema<VA, VI, VR>
}): Schema<Map<KA, VA>, { readonly [x: string]: VI }, KR | VR> =>
transform(Record(encodedBoundSchema(key), value), MapFromSelf({ key, value: typeSchema(value) }), {
decode: (record) => new Map(Object.entries(record)),
encode: record_.fromEntries
})

const setArbitrary = <A>(item: LazyArbitrary<A>): LazyArbitrary<ReadonlySet<A>> => (fc) =>
fc.array(item(fc)).map((as) => new Set(as))

Expand Down
34 changes: 34 additions & 0 deletions packages/schema/test/JSONSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2106,6 +2106,40 @@ schema (Suspend): <suspended schema>`
}
)
})

it("ReadonlyMapFromRecord", () => {
expectJSONSchema(
Schema.ReadonlyMapFromRecord({
key: Schema.String.pipe(Schema.minLength(2, { jsonSchema: { pattern: ".{2,}" } })),
value: Schema.NumberFromString
}),
{
"$schema": "http://json-schema.org/draft-07/schema#",
type: "object",
required: [],
properties: {},
additionalProperties: false,
patternProperties: { ".{2,}": { type: "string" } }
}
)
})

it("MapFromRecord", () => {
expectJSONSchema(
Schema.MapFromRecord({
key: Schema.String.pipe(Schema.minLength(2, { jsonSchema: { pattern: ".{2,}" } })),
value: Schema.NumberFromString
}),
{
"$schema": "http://json-schema.org/draft-07/schema#",
type: "object",
required: [],
properties: {},
additionalProperties: false,
patternProperties: { ".{2,}": { type: "string" } }
}
)
})
})

export const decode = <A>(schema: JSONSchema.JsonSchema7Root): Schema.Schema<A> =>
Expand Down
54 changes: 54 additions & 0 deletions packages/schema/test/Schema/Map/MapFromRecord.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as S from "@effect/schema/Schema"
import * as Util from "@effect/schema/test/TestUtils"
import { describe, it } from "vitest"

describe("MapFromRecord", () => {
it("decoding", async () => {
const schema = S.MapFromRecord({ key: S.NumberFromString, value: S.NumberFromString })
await Util.expectDecodeUnknownSuccess(schema, {}, new Map())
await Util.expectDecodeUnknownSuccess(
schema,
{ 1: "2", 3: "4", 5: "6" },
new Map([[1, 2], [3, 4], [5, 6]])
)

await Util.expectDecodeUnknownFailure(
schema,
null,
`({ readonly [x: string]: NumberFromString } <-> Map<NumberFromString, number>)
└─ Encoded side transformation failure
└─ Expected { readonly [x: string]: NumberFromString }, actual null`
)
await Util.expectDecodeUnknownFailure(
schema,
{ a: "1" },
`({ readonly [x: string]: NumberFromString } <-> Map<NumberFromString, number>)
└─ Type side transformation failure
└─ Map<NumberFromString, number>
└─ ReadonlyArray<readonly [NumberFromString, number]>
└─ [0]
└─ readonly [NumberFromString, number]
└─ [0]
└─ NumberFromString
└─ Transformation process failure
└─ Expected NumberFromString, actual "a"`
)
await Util.expectDecodeUnknownFailure(
schema,
{ 1: "a" },
`({ readonly [x: string]: NumberFromString } <-> Map<NumberFromString, number>)
└─ Encoded side transformation failure
└─ { readonly [x: string]: NumberFromString }
└─ ["1"]
└─ NumberFromString
└─ Transformation process failure
└─ Expected NumberFromString, actual "a"`
)
})

it("encoding", async () => {
const schema = S.MapFromRecord({ key: S.NumberFromString, value: S.NumberFromString })
await Util.expectEncodeSuccess(schema, new Map(), {})
await Util.expectEncodeSuccess(schema, new Map([[1, 2], [3, 4], [5, 6]]), { 1: "2", 3: "4", 5: "6" })
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as S from "@effect/schema/Schema"
import * as Util from "@effect/schema/test/TestUtils"
import { describe, it } from "vitest"

describe("ReadonlyMapFromRecord", () => {
it("decoding", async () => {
const schema = S.ReadonlyMapFromRecord({ key: S.NumberFromString, value: S.NumberFromString })
await Util.expectDecodeUnknownSuccess(schema, {}, new Map())
await Util.expectDecodeUnknownSuccess(
schema,
{ 1: "2", 3: "4", 5: "6" },
new Map([[1, 2], [3, 4], [5, 6]])
)

await Util.expectDecodeUnknownFailure(
schema,
null,
`({ readonly [x: string]: NumberFromString } <-> ReadonlyMap<NumberFromString, number>)
└─ Encoded side transformation failure
└─ Expected { readonly [x: string]: NumberFromString }, actual null`
)
await Util.expectDecodeUnknownFailure(
schema,
{ a: "1" },
`({ readonly [x: string]: NumberFromString } <-> ReadonlyMap<NumberFromString, number>)
└─ Type side transformation failure
└─ ReadonlyMap<NumberFromString, number>
└─ ReadonlyArray<readonly [NumberFromString, number]>
└─ [0]
└─ readonly [NumberFromString, number]
└─ [0]
└─ NumberFromString
└─ Transformation process failure
└─ Expected NumberFromString, actual "a"`
)
await Util.expectDecodeUnknownFailure(
schema,
{ 1: "a" },
`({ readonly [x: string]: NumberFromString } <-> ReadonlyMap<NumberFromString, number>)
└─ Encoded side transformation failure
└─ { readonly [x: string]: NumberFromString }
└─ ["1"]
└─ NumberFromString
└─ Transformation process failure
└─ Expected NumberFromString, actual "a"`
)
})

it("encoding", async () => {
const schema = S.ReadonlyMapFromRecord({ key: S.NumberFromString, value: S.NumberFromString })
await Util.expectEncodeSuccess(schema, new Map(), {})
await Util.expectEncodeSuccess(schema, new Map([[1, 2], [3, 4], [5, 6]]), { 1: "2", 3: "4", 5: "6" })
})
})

0 comments on commit aa50781

Please sign in to comment.