Skip to content

Commit

Permalink
Add support for symbols to Issue in platform/HttpApiError
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti committed Jan 15, 2025
1 parent a4a5b87 commit c8887be
Show file tree
Hide file tree
Showing 11 changed files with 252 additions and 132 deletions.
5 changes: 5 additions & 0 deletions .changeset/two-lies-end.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": patch
---

Relocate the `Issue` definition from `platform/HttpApiError` to `Schema` (renamed as `ArrayFormatterIssue`).
5 changes: 5 additions & 0 deletions .changeset/young-trains-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/platform": minor
---

Add support for symbols in the `Issue` definition within `platform/HttpApiError`.
13 changes: 13 additions & 0 deletions packages/effect/src/ParseResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1935,12 +1935,25 @@ const formatTree = (
}

/**
* Represents an issue returned by the {@link ArrayFormatter} formatter.
*
* @category model
* @since 3.10.0
*/
export interface ArrayFormatterIssue {
/**
* The tag identifying the type of parse issue.
*/
readonly _tag: ParseIssue["_tag"]

/**
* The path to the property where the issue occurred.
*/
readonly path: ReadonlyArray<PropertyKey>

/**
* A descriptive message explaining the issue.
*/
readonly message: string
}

Expand Down
70 changes: 61 additions & 9 deletions packages/effect/src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5189,21 +5189,25 @@ export class Not extends transform(Boolean$.annotations({ description: "a boolea
encode: boolean_.not
}) {}

const encodeSymbol = (sym: symbol, _: AST.ParseOptions, ast: AST.AST) => {
const key = Symbol.keyFor(sym)
return key === undefined
? ParseResult.fail(
new ParseResult.Type(ast, sym, `Unable to encode a unique symbol ${String(sym)} into a string`)
)
: ParseResult.succeed(key)
}

const decodeSymbol = (s: string) => ParseResult.succeed(Symbol.for(s))

/** @ignore */
class Symbol$ extends transformOrFail(
String$.annotations({ description: "a string to be decoded into a globally shared symbol" }),
SymbolFromSelf,
{
strict: false,
decode: (s) => ParseResult.succeed(Symbol.for(s)),
encode: (sym, _, ast) => {
const key = Symbol.keyFor(sym)
return key === undefined
? ParseResult.fail(
new ParseResult.Type(ast, sym, `Unable to encode a unique symbol ${String(sym)} into a string`)
)
: ParseResult.succeed(key)
}
decode: decodeSymbol,
encode: encodeSymbol
}
).annotations({ identifier: "Symbol" }) {}

Expand All @@ -5217,6 +5221,20 @@ export {
Symbol$ as Symbol
}

const SymbolStruct = TaggedStruct("symbol", {
key: String$
}).annotations({ description: "an object to be decoded into a globally shared symbol" })

const SymbolFromStruct = transformOrFail(
SymbolStruct,
SymbolFromSelf,
{
strict: true,
decode: ({ key }) => decodeSymbol(key),
encode: (sym, _, ast) => ParseResult.map(encodeSymbol(sym, _, ast), (key) => SymbolStruct.make({ key }))
}
)

/**
* @category schema id
* @since 3.10.0
Expand Down Expand Up @@ -10390,3 +10408,37 @@ const go = (ast: AST.AST, path: ReadonlyArray<PropertyKey>): Equivalence.Equival
}
}
}

/** @ignore */
class PropertyKey$ extends Union(String$, Number$, SymbolFromStruct).annotations({ identifier: "PropertyKey" }) {}

export {
/**
* @since 3.12.5
*/
PropertyKey$ as PropertyKey
}

/**
* @category ArrayFormatter
* @since 3.12.5
*/
export class ArrayFormatterIssue extends Struct({
_tag: propertySignature(Literal(
"Pointer",
"Unexpected",
"Missing",
"Composite",
"Refinement",
"Transformation",
"Type",
"Forbidden"
)).annotations({ description: "The tag identifying the type of parse issue" }),
path: propertySignature(Array$(PropertyKey$)).annotations({
description: "The path to the property where the issue occurred"
}),
message: propertySignature(String$).annotations({ description: "A descriptive message explaining the issue" })
}).annotations({
identifier: "ArrayFormatterIssue",
description: "Represents an issue returned by the ArrayFormatter formatter"
}) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as S from "effect/Schema"
import * as Util from "effect/test/Schema/TestUtils"
import { describe, it } from "vitest"

describe("ArrayFormatterIssue", () => {
const schema = S.ArrayFormatterIssue

it("property tests", () => {
Util.roundtrip(schema)
})
})
16 changes: 16 additions & 0 deletions packages/effect/test/Schema/Schema/PropertyKey/PropertyKey.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as Schema from "effect/Schema"
import { describe, expect, it } from "vitest"

describe("PropertyKey", () => {
it("should handle symbol, string, and number", () => {
const encodeSync = Schema.encodeSync(Schema.PropertyKey)
const decodeSync = Schema.decodeSync(Schema.PropertyKey)
const expectRoundtrip = (pk: PropertyKey) => {
expect(decodeSync(encodeSync(pk))).toStrictEqual(pk)
}

expectRoundtrip("path")
expectRoundtrip(1)
expectRoundtrip(Symbol.for("symbol"))
})
})
7 changes: 2 additions & 5 deletions packages/effect/test/Schema/TestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ import * as AST from "effect/SchemaAST"
import * as fc from "fast-check"
import { assert, expect } from "vitest"

const doEffectify = true
const doRoundtrip = false

export const sleep = Effect.sleep(Duration.millis(10))

const effectifyDecode = <R>(
Expand Down Expand Up @@ -124,7 +121,7 @@ export const expectArbitrary = <A, I>(schema: S.Schema<A, I, never>, n: number =
}

export const roundtrip = <A, I>(schema: S.Schema<A, I, never>, params?: Parameters<typeof fc.assert>[1]) => {
if (!doRoundtrip) {
if (true as boolean) {
return
}
const arb = A.makeLazy(schema)
Expand All @@ -146,7 +143,7 @@ export const roundtrip = <A, I>(schema: S.Schema<A, I, never>, params?: Paramete
}),
params
)
if (doEffectify) {
if (true as boolean) {
const effectSchema = effectify(schema)
const encode = S.encode(effectSchema)
const decode = S.decode(effectSchema)
Expand Down
97 changes: 59 additions & 38 deletions packages/platform-node/test/fixtures/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -531,53 +531,74 @@
"issues": {
"type": "array",
"items": {
"type": "object",
"required": ["_tag", "path", "message"],
"properties": {
"_tag": {
"type": "string",
"enum": [
"Pointer",
"Unexpected",
"Missing",
"Composite",
"Refinement",
"Transformation",
"Type",
"Forbidden"
]
},
"path": {
"type": "array",
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "number"
}
]
}
},
"message": {
"type": "string"
}
},
"additionalProperties": false
"$ref": "#/components/schemas/Issue"
}
},
"message": {
"type": "string"
},
"message": { "type": "string" },
"_tag": {
"type": "string",
"enum": ["HttpApiDecodeError"]
"enum": [
"HttpApiDecodeError"
]
}
},
"additionalProperties": false,
"description": "The request did not match the expected schema"
},
"Issue": {
"type": "object",
"description": "Represents an error encountered while parsing a value to match the schema",
"required": ["_tag", "path", "message"],
"properties": {
"_tag": {
"type": "string",
"description": "The tag identifying the type of parse issue",
"enum": [
"Pointer",
"Unexpected",
"Missing",
"Composite",
"Refinement",
"Transformation",
"Type",
"Forbidden"
]
},
"path": {
"type": "array",
"description": "The path to the property where the issue occurred",
"items": {
"$ref": "#/components/schemas/PropertyKey"
}
},
"message": {
"type": "string",
"description": "A descriptive message explaining the issue"
}
},
"additionalProperties": false
},
"PropertyKey": {
"anyOf": [
{ "type": "string" },
{ "type": "number" },
{
"additionalProperties": false,
"description": "an object to be decoded into a globally shared symbol",
"properties": {
"_tag": {
"enum": [
"symbol"
],
"type": "string"
},
"key": { "type": "string" }
},
"required": ["_tag", "key"],
"type": "object"
}
]
},
"GlobalError": {
"type": "object",
"required": ["_tag"],
Expand Down
59 changes: 4 additions & 55 deletions packages/platform/src/HttpApiError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
* @since 1.0.0
*/
import * as Effect from "effect/Effect"
import { identity } from "effect/Function"
import * as ParseResult from "effect/ParseResult"
import * as Schema from "effect/Schema"
import * as HttpApiSchema from "./HttpApiSchema.js"
Expand All @@ -23,60 +22,10 @@ export type TypeId = typeof TypeId
* @since 1.0.0
* @category schemas
*/
export interface Issue extends
Schema.Struct<
{
_tag: Schema.Literal<
["Pointer", "Unexpected", "Missing", "Composite", "Refinement", "Transformation", "Type", "Forbidden"]
>
path: PropertyKeysNoSymbol
message: typeof Schema.String
}
>
{}

/**
* @since 1.0.0
* @category schemas
*/
export interface PropertyKeysNoSymbol extends
Schema.transform<
Schema.Array$<Schema.Union<[typeof Schema.String, typeof Schema.Number]>>,
Schema.Array$<Schema.Union<[typeof Schema.SymbolFromSelf, typeof Schema.String, typeof Schema.Number]>>
>
{}

/**
* @since 1.0.0
* @category schemas
*/
export const PropertyKeysNoSymbol: PropertyKeysNoSymbol = Schema.transform(
Schema.Array(Schema.Union(Schema.String, Schema.Number)),
Schema.Array(Schema.Union(Schema.SymbolFromSelf, Schema.String, Schema.Number)),
{
decode: identity,
encode: (items) => items.filter((item) => typeof item !== "symbol")
}
)

/**
* @since 1.0.0
* @category schemas
*/
export const Issue: Issue = Schema.Struct({
_tag: Schema.Literal(
"Pointer",
"Unexpected",
"Missing",
"Composite",
"Refinement",
"Transformation",
"Type",
"Forbidden"
),
path: PropertyKeysNoSymbol,
message: Schema.String
})
export class Issue extends Schema.ArrayFormatterIssue.annotations({
identifier: "Issue",
description: "Represents an error encountered while parsing a value to match the schema"
}) {}

/**
* @since 1.0.0
Expand Down
21 changes: 21 additions & 0 deletions packages/platform/test/HttpApiError.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as HttpApiError from "@effect/platform/HttpApiError"
import * as Schema from "effect/Schema"
import { describe, expect, it } from "vitest"

describe("HttpApiError", () => {
describe("Issue schema", () => {
it("path should handle symbol, string, and number", () => {
const encodeSync = Schema.encodeSync(HttpApiError.Issue)
const decodeSync = Schema.decodeSync(HttpApiError.Issue)
const expectRoundtrip = (issue: typeof HttpApiError.Issue.Type) => {
expect(decodeSync(encodeSync(issue))).toStrictEqual(issue)
}

expectRoundtrip({
_tag: "Pointer",
path: ["path", 1, Symbol.for("symbol")],
message: "message"
})
})
})
})
Loading

0 comments on commit c8887be

Please sign in to comment.