Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .agents/skills/jsdocs/declarations.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,13 @@ Declaration tags appear in this order:
1. `@deprecated`
2. `@default`
3. `@see`
4. `@category`
5. `@since`
4. `@unstable`
5. `@category`
6. `@since`

- Roots require stable-semver `@since` and no `@default`; category requirements
live in [categories.md](categories.md).
live in [categories.md](categories.md). Root declarations may include one
valueless `@unstable` marker.
- Namespaces and their declarations require stable-semver `@since`, permit
`@category`, and reject `@default`.
- Member JSDoc is optional; when present it permits stable-semver `@since` and
Expand Down
7 changes: 7 additions & 0 deletions .changeset/parsed-media-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"effect": patch
"@effect/jsdocs": patch
"@effect/openapi-generator": patch
---

Add a parsed `effect/unstable/http/MediaType` value with validated construction, deterministic formatting, input conversion, parameter and RFC 6838 structured-suffix access, charset-aware matching, common JSON/XML/text predicates, `Schema.MediaType` codecs, and `Config.MediaType` support. `HttpApiSchema` content-type options accept media type values, strings, or structured parts and normalize them into parsed encoding metadata. `PayloadEncoding["contentType"]`, `ResponseEncoding["contentType"]`, and `StreamSchema["contentType"]` now return `MediaType` values instead of strings; use `MediaType.format` when a string is required. HTTP API dispatch, response selection, multipart parsing, and OpenAPI generation compare validated media-type essences. Missing response content types remain distinct from empty or malformed fields, and malformed request content types continue to receive a 415 response. OpenAPI-generated HTTP APIs pass custom content-type strings through these normalized input boundaries. The JSDoc checker now accepts the valueless `@unstable` marker on public declarations.
15 changes: 15 additions & 0 deletions packages/effect/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1531,6 +1531,21 @@ export function URL(name?: string) {
return schema(Schema.URL, name)
}

/**
* Creates a config for a normalized HTTP media type parsed from a string.
*
* **Details**
*
* This is a shortcut for `Config.schema(Schema.MediaType, name)`.
*
* @unstable
* @category constructors
* @since 4.0.0
*/
export function MediaType(name?: string) {
return schema(Schema.MediaType, name)
}

/**
* Creates a config for a `Date` value parsed from a string.
*
Expand Down
87 changes: 87 additions & 0 deletions packages/effect/src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import type { Assign, Lambda, Mutable, Simplify } from "./Struct.ts"
import * as Struct_ from "./Struct.ts"
import type { RequiredKeys, UnionToIntersection } from "./Types.ts"
import type { Unify } from "./Unify.ts"
import * as MediaType_ from "./unstable/http/MediaType.ts"

const TypeId = InternalSchema.TypeId

Expand Down Expand Up @@ -11888,6 +11889,92 @@ export interface URLFromString extends decodeTo<URL, String> {
*/
export const URLFromString: URLFromString = URLString.pipe(decodeTo(URL, SchemaTransformation.urlFromString))

/**
* Type-level representation of {@link MediaType}.
*
* @unstable
* @category models
* @since 4.0.0
*/
export interface MediaType extends declare<MediaType_.MediaType> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this live under unstable/http instead? This adds an unstable MediaType type to the stable Schema and Config API.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, this should just add a @unstable tag instead but stay in Schema.ts

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept these APIs in Schema and Config as discussed, and marked the declarations with @unstable.

readonly "Rebuild": MediaType
}

const MediaTypeString = String.annotate({ expected: "a string that will be decoded as an HTTP media type" })

const mediaTypeTransformation = SchemaTransformation.transformOrFail({
decode: (input: string, options) => {
const result = MediaType_.parse(input)
return Result_.isFailure(result)
? Effect.fail(
new SchemaIssue.InvalidValue(
{ message: `${result.failure.message} at offset ${result.failure.offset}` },
input,
options
)
)
: Effect.succeed(result.success)
},
encode: (mediaType: MediaType_.MediaType) => Effect.succeed(MediaType_.format(mediaType))
})

/**
* Schema for parsed HTTP media-type values.
*
* @see {@link MediaTypeFromString} for decoding media types from strings
*
* @unstable
* @category schemas
* @since 4.0.0
*/
export const MediaType: MediaType = declare(MediaType_.isMediaType, {
representation: { id: "effect/schema/MediaType", payload: null },
toCode: () => ({
runtime: "Schema.MediaType",
Type: "MediaType.MediaType",
importDeclarations: [`import * as MediaType from "effect/unstable/http/MediaType"`]
}),
expected: "MediaType",
toEquivalence: () => MediaType_.Equivalence,
toCodecJson: () => link<MediaType_.MediaType>()(MediaTypeString, mediaTypeTransformation)
})

/**
* Reviver for persisted {@link MediaType} declarations.
*
* @unstable
* @category schemas
* @since 4.0.0
*/
export const MediaTypeReviver = makeFixedDeclarationReviver(
"effect/schema/MediaType",
MediaType
)

/**
* Type-level representation of {@link MediaTypeFromString}.
*
* @unstable
* @category models
* @since 4.0.0
*/
export interface MediaTypeFromString extends decodeTo<MediaType, String> {
readonly "Rebuild": MediaTypeFromString
}

/**
* Schema that decodes strings into normalized HTTP media-type values.
*
* @see {@link MediaType} for validating already parsed media types
*
* @unstable
* @category schemas
* @since 4.0.0
*/
export const MediaTypeFromString: MediaTypeFromString = MediaTypeString.pipe(
decodeTo(MediaType, mediaTypeTransformation)
)

/**
* Type-level representation of {@link Date}.
*
Expand Down
8 changes: 5 additions & 3 deletions packages/effect/src/unstable/http/HttpServerRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,9 +251,11 @@ export const schemaBodyJson = <A, RD>(
return Effect.flatMap(HttpServerRequest, parse)
}

const isMultipart = (request: HttpServerRequest) =>
request.headers["content-type"]?.toLowerCase().includes("multipart/form-data") === true ||
getFormDataBody(request) !== undefined
const isMultipart = (request: HttpServerRequest) => {
const contentType = request.headers["content-type"]
return contentType?.toLowerCase().includes("multipart/form-data") === true ||
getFormDataBody(request) !== undefined
}

/**
* Decodes the current request body as form data.
Expand Down
Loading
Loading