Skip to content

Commit

Permalink
porting of #3792
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti committed Oct 16, 2024
1 parent da0e68d commit b24b9f9
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 98 deletions.
189 changes: 109 additions & 80 deletions packages/effect/src/SchemaAST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2117,51 +2117,57 @@ export const getNumberIndexedAccess = (ast: AST): AST => {
throw new Error(errors_.getASTUnsupportedSchema(ast))
}

const getTypeLiteralPropertySignature = (ast: TypeLiteral, name: PropertyKey): PropertySignature | undefined => {
// from property signatures...
const ops = Arr.findFirst(ast.propertySignatures, (ps) => ps.name === name)
if (Option.isSome(ops)) {
return ops.value
}

// from index signatures...
if (Predicate.isString(name)) {
let out: PropertySignature | undefined = undefined
for (const is of ast.indexSignatures) {
const parameterBase = getParameterBase(is.parameter)
switch (parameterBase._tag) {
case "TemplateLiteral": {
const regex = getTemplateLiteralRegExp(parameterBase)
if (regex.test(name)) {
return new PropertySignature(name, is.type, false, true)
}
break
}
case "StringKeyword": {
if (out === undefined) {
out = new PropertySignature(name, is.type, false, true)
}
}
}
}
if (out) {
return out
}
} else if (Predicate.isSymbol(name)) {
for (const is of ast.indexSignatures) {
const parameterBase = getParameterBase(is.parameter)
if (isSymbolKeyword(parameterBase)) {
return new PropertySignature(name, is.type, false, true)
}
}
}
}

/** @internal */
export const getPropertyKeyIndexedAccess = (ast: AST, name: PropertyKey): PropertySignature => {
const annotation = getSurrogateAnnotation(ast)
if (Option.isSome(annotation)) {
return getPropertyKeyIndexedAccess(annotation.value, name)
}
switch (ast._tag) {
case "Declaration": {
const annotation = getSurrogateAnnotation(ast)
if (Option.isSome(annotation)) {
return getPropertyKeyIndexedAccess(annotation.value, name)
}
break
}
case "TypeLiteral": {
const ops = Arr.findFirst(ast.propertySignatures, (ps) => ps.name === name)
if (Option.isSome(ops)) {
return ops.value
} else {
if (Predicate.isString(name)) {
let out: PropertySignature | undefined = undefined
for (const is of ast.indexSignatures) {
const parameterBase = getParameterBase(is.parameter)
switch (parameterBase._tag) {
case "TemplateLiteral": {
const regex = getTemplateLiteralRegExp(parameterBase)
if (regex.test(name)) {
return new PropertySignature(name, is.type, false, true)
}
break
}
case "StringKeyword": {
if (out === undefined) {
out = new PropertySignature(name, is.type, false, true)
}
}
}
}
if (out) {
return out
}
} else if (Predicate.isSymbol(name)) {
for (const is of ast.indexSignatures) {
const parameterBase = getParameterBase(is.parameter)
if (isSymbolKeyword(parameterBase)) {
return new PropertySignature(name, is.type, false, true)
}
}
}
const ps = getTypeLiteralPropertySignature(ast, name)
if (ps) {
return ps
}
break
}
Expand All @@ -2174,19 +2180,18 @@ export const getPropertyKeyIndexedAccess = (ast: AST, name: PropertyKey): Proper
)
case "Suspend":
return getPropertyKeyIndexedAccess(ast.f(), name)
case "Refinement":
return getPropertyKeyIndexedAccess(ast.from, name)
}
return new PropertySignature(name, neverKeyword, false, true)
throw new Error(errors_.getASTUnsupportedSchema(ast))
}

const getPropertyKeys = (ast: AST): Array<PropertyKey> => {
const annotation = getSurrogateAnnotation(ast)
if (Option.isSome(annotation)) {
return getPropertyKeys(annotation.value)
}
switch (ast._tag) {
case "Declaration": {
const annotation = getSurrogateAnnotation(ast)
if (Option.isSome(annotation)) {
return getPropertyKeys(annotation.value)
}
break
}
case "TypeLiteral":
return ast.propertySignatures.map((ps) => ps.name)
case "Suspend":
Expand Down Expand Up @@ -2252,44 +2257,68 @@ export const record = (key: AST, value: AST): {
* @since 3.10.0
*/
export const pick = (ast: AST, keys: ReadonlyArray<PropertyKey>): TypeLiteral | Transformation => {
if (isTransformation(ast)) {
switch (ast.transformation._tag) {
case "ComposeTransformation":
return new Transformation(
pick(ast.from, keys),
pick(ast.to, keys),
composeTransformation
)
case "TypeLiteralTransformation": {
const ts: Array<PropertySignatureTransformation> = []
const fromKeys: Array<PropertyKey> = []
for (const k of keys) {
const t = ast.transformation.propertySignatureTransformations.find((t) => t.to === k)
if (t) {
ts.push(t)
fromKeys.push(t.from)
} else {
fromKeys.push(k)
const annotation = getSurrogateAnnotation(ast)
if (Option.isSome(annotation)) {
return pick(annotation.value, keys)
}
switch (ast._tag) {
case "TypeLiteral": {
const pss: Array<PropertySignature> = []
const names: Record<PropertyKey, null> = {}
for (const ps of ast.propertySignatures) {
names[ps.name] = null
if (keys.includes(ps.name)) {
pss.push(ps)
}
}
for (const key of keys) {
if (!(key in names)) {
const ps = getTypeLiteralPropertySignature(ast, key)
if (ps) {
pss.push(ps)
}
}
return Arr.isNonEmptyReadonlyArray(ts) ?
new Transformation(
pick(ast.from, fromKeys),
pick(ast.to, keys),
new TypeLiteralTransformation(ts)
) :
pick(ast.from, fromKeys)
}
case "FinalTransformation": {
const annotation = getSurrogateAnnotation(ast)
if (Option.isSome(annotation)) {
return pick(annotation.value, keys)
return new TypeLiteral(pss, [])
}
case "Union":
return new TypeLiteral(keys.map((name) => getPropertyKeyIndexedAccess(ast, name)), [])
case "Suspend":
return pick(ast.f(), keys)
case "Refinement":
return pick(ast.from, keys)
case "Transformation": {
switch (ast.transformation._tag) {
case "ComposeTransformation":
return new Transformation(
pick(ast.from, keys),
pick(ast.to, keys),
composeTransformation
)
case "TypeLiteralTransformation": {
const ts: Array<PropertySignatureTransformation> = []
const fromKeys: Array<PropertyKey> = []
for (const k of keys) {
const t = ast.transformation.propertySignatureTransformations.find((t) => t.to === k)
if (t) {
ts.push(t)
fromKeys.push(t.from)
} else {
fromKeys.push(k)
}
}
return Arr.isNonEmptyReadonlyArray(ts) ?
new Transformation(
pick(ast.from, fromKeys),
pick(ast.to, keys),
new TypeLiteralTransformation(ts)
) :
pick(ast.from, fromKeys)
}
throw new Error(errors_.getASTUnsupportedSchema(ast))
}
}
}
return new TypeLiteral(keys.map((key) => getPropertyKeyIndexedAccess(ast, key)), [])
throw new Error(errors_.getASTUnsupportedSchema(ast))
}

/**
Expand Down
24 changes: 12 additions & 12 deletions packages/effect/test/Schema/Schema/pick.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest"

describe("pick", () => {
it("Struct", async () => {
const a = Symbol.for("effect/Schema/test/a")
const a = Symbol.for("@effect/schema/test/a")
const schema = S.Struct({ [a]: S.String, b: S.NumberFromString, c: S.Boolean }).pipe(
S.pick(a, "b")
)
Expand All @@ -13,20 +13,20 @@ describe("pick", () => {
await Util.expectDecodeUnknownFailure(
schema,
null,
"Expected { readonly Symbol(effect/Schema/test/a): string; readonly b: NumberFromString }, actual null"
"Expected { readonly b: NumberFromString; readonly Symbol(@effect/schema/test/a): string }, actual null"
)
await Util.expectDecodeUnknownFailure(
schema,
{ [a]: "a" },
`{ readonly Symbol(effect/Schema/test/a): string; readonly b: NumberFromString }
`{ readonly b: NumberFromString; readonly Symbol(@effect/schema/test/a): string }
└─ ["b"]
└─ is missing`
)
await Util.expectDecodeUnknownFailure(
schema,
{ b: 1 },
`{ readonly Symbol(effect/Schema/test/a): string; readonly b: NumberFromString }
└─ [Symbol(effect/Schema/test/a)]
{ b: "1" },
`{ readonly b: NumberFromString; readonly Symbol(@effect/schema/test/a): string }
└─ [Symbol(@effect/schema/test/a)]
└─ is missing`
)
})
Expand Down Expand Up @@ -113,22 +113,22 @@ describe("pick", () => {
})

it("Record(symbol, number)", async () => {
const a = Symbol.for("effect/Schema/test/a")
const b = Symbol.for("effect/Schema/test/b")
const a = Symbol.for("@effect/schema/test/a")
const b = Symbol.for("@effect/schema/test/b")
const schema = S.Record({ key: S.SymbolFromSelf, value: S.Number }).pipe(S.pick(a, b))
await Util.expectDecodeUnknownSuccess(schema, { [a]: 1, [b]: 2 })
await Util.expectDecodeUnknownFailure(
schema,
{ [a]: "a", [b]: 2 },
`{ readonly Symbol(effect/Schema/test/a): number; readonly Symbol(effect/Schema/test/b): number }
└─ [Symbol(effect/Schema/test/a)]
`{ readonly Symbol(@effect/schema/test/a): number; readonly Symbol(@effect/schema/test/b): number }
└─ [Symbol(@effect/schema/test/a)]
└─ Expected number, actual "a"`
)
await Util.expectDecodeUnknownFailure(
schema,
{ [a]: 1, [b]: "b" },
`{ readonly Symbol(effect/Schema/test/a): number; readonly Symbol(effect/Schema/test/b): number }
└─ [Symbol(effect/Schema/test/b)]
`{ readonly Symbol(@effect/schema/test/a): number; readonly Symbol(@effect/schema/test/b): number }
└─ [Symbol(@effect/schema/test/b)]
└─ Expected number, actual "b"`
)
})
Expand Down
47 changes: 41 additions & 6 deletions packages/effect/test/Schema/SchemaAST/pick.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,36 @@ import * as AST from "effect/SchemaAST"
import { describe, expect, it } from "vitest"

describe("pick", () => {
it("TypeLiteral", async () => {
it("refinement", async () => {
const schema = S.Struct({ a: S.NumberFromString, b: S.Number }).pipe(S.filter(() => true))
const ast = schema.pipe(S.pick("a")).ast
expect(ast).toStrictEqual(S.Struct({ a: S.NumberFromString }).ast)
})

it("struct", async () => {
const schema = S.Struct({ a: S.NumberFromString, b: S.Number })
const ast = schema.pipe(S.pick("a")).ast
expect(ast).toStrictEqual(S.Struct({ a: S.NumberFromString }).ast)
})

it("struct + record", async () => {
const schema = S.Struct(
{ a: S.NumberFromString, b: S.Number },
S.Record({ key: S.String, value: S.Union(S.String, S.Number) })
)
const ast = schema.pipe(S.pick("a", "c")).ast
expect(ast).toStrictEqual(S.Struct({ a: S.NumberFromString, c: S.Union(S.String, S.Number) }).ast)
})

it("union", async () => {
const A = S.Struct({ a: S.String })
const B = S.Struct({ a: S.Number })
const schema = S.Union(A, B)
const pick = schema.pipe(S.pick("a"))
const ast = pick.ast
expect(ast).toStrictEqual(S.Struct({ a: S.Union(S.String, S.Number) }).ast)
})

describe("transformation", () => {
it("ComposeTransformation", async () => {
const schema = S.compose(
Expand Down Expand Up @@ -41,11 +65,22 @@ describe("pick", () => {
})
})

it("with SurrogateAnnotation", async () => {
class A extends S.Class<A>("A")({ a: S.NumberFromString, b: S.Number }) {}
const schema = A
const ast = schema.pipe(S.pick("a")).ast
expect(ast).toStrictEqual(S.Struct({ a: S.NumberFromString }).ast)
describe("SurrogateAnnotation", () => {
it("a single Class", async () => {
class A extends S.Class<A>("A")({ a: S.NumberFromString, b: S.Number }) {}
const schema = A
const ast = schema.pipe(S.pick("a")).ast
expect(ast).toStrictEqual(S.Struct({ a: S.NumberFromString }).ast)
})

it("a union of Classes", async () => {
class A extends S.Class<A>("A")({ a: S.Number }) {}
class B extends S.Class<B>("B")({ a: S.String }) {}
const schema = S.Union(A, B)
const pick = schema.pipe(S.pick("a"))
const ast = pick.ast
expect(ast).toStrictEqual(S.Struct({ a: S.Union(S.Number, S.String) }).ast)
})
})
})
})

0 comments on commit b24b9f9

Please sign in to comment.