EdgeAIG's picture
download
raw
20.7 kB
/**
* Builds HTTP clients from `HttpApi` declarations.
*
* The client methods are derived from the groups and endpoints in an `HttpApi`
* and run through an `HttpClient`. They use the same schema-driven contract as
* the server: request parts are encoded from endpoint schemas, client
* middleware is applied, the HTTP request is executed, and declared success or
* error responses are decoded. This module also includes helpers for building a
* client for only one group, one endpoint, or only the encoded URL.
*
* @since 4.0.0
*/
import * as Arr from "../../Array.js";
import * as Cause from "../../Cause.js";
import * as Effect from "../../Effect.js";
import { identity } from "../../Function.js";
import * as Option from "../../Option.js";
import * as Predicate from "../../Predicate.js";
import * as Schema from "../../Schema.js";
import * as SchemaAST from "../../SchemaAST.js";
import * as SchemaIssue from "../../SchemaIssue.js";
import * as SchemaTransformation from "../../SchemaTransformation.js";
import * as Stream from "../../Stream.js";
import * as UndefinedOr from "../../UndefinedOr.js";
import * as Sse from "../encoding/Sse.js";
import * as HttpBody from "../http/HttpBody.js";
import * as HttpClient from "../http/HttpClient.js";
import * as HttpClientError from "../http/HttpClientError.js";
import * as HttpClientRequest from "../http/HttpClientRequest.js";
import * as HttpClientResponse from "../http/HttpClientResponse.js";
import * as HttpMethod from "../http/HttpMethod.js";
import * as UrlParams from "../http/UrlParams.js";
import * as HttpApi from "./HttpApi.js";
import * as HttpApiEndpoint from "./HttpApiEndpoint.js";
import * as HttpApiSchema from "./HttpApiSchema.js";
/** @internal */
export const makeClient = (api, options) => Effect.gen(function* () {
const services = yield* Effect.context();
const httpClient = options.httpClient.pipe(options?.baseUrl === undefined ? identity : HttpClient.mapRequest(HttpClientRequest.prependUrl(options.baseUrl.toString())));
function executeMiddleware(group, endpoint, request, middlewareKeys, index) {
if (index === -1) {
return httpClient.execute(request);
}
const middleware = services.mapUnsafe.get(middlewareKeys[index]);
if (middleware === undefined) {
return executeMiddleware(group, endpoint, request, middlewareKeys, index - 1);
}
return middleware({
endpoint,
group,
request,
next(request) {
return executeMiddleware(group, endpoint, request, middlewareKeys, index - 1);
}
});
}
HttpApi.reflect(api, {
predicate: options?.predicate,
onGroup(onGroupOptions) {
options.onGroup?.(onGroupOptions);
},
onEndpoint(onEndpointOptions) {
const {
group,
endpoint,
errors,
successes
} = onEndpointOptions;
const makeUrl = compilePath(endpoint.path);
const decodeMap = {
orElse: statusOrElse
};
const decodeResponse = HttpClientResponse.matchStatus(decodeMap);
errors.forEach((schemas, status) => {
// decoders
const decode = schemasToResponse(schemas);
decodeMap[status] = response => Effect.flatMap(Effect.catchCause(decode(response), cause => Effect.failCause(Cause.combine(Cause.fail(new HttpClientError.HttpClientError({
reason: new HttpClientError.StatusCodeError({
request: response.request,
response
})
})), cause))), Effect.fail);
});
const successAlternatives = new Map();
for (const [status, schemas] of successes.entries()) {
const grouped = groupSchemasByContentType(schemas);
for (const [contentType, schemas] of grouped.entries()) {
addResponseAlternative(successAlternatives, status, contentType, schemasToResponse(schemas));
}
}
for (const streamSuccess of getStreamSuccessSchemas(endpoint)) {
addResponseAlternative(successAlternatives, HttpApiSchema.getStatusStream(streamSuccess), streamSuccess.contentType, streamToResponse(streamSuccess));
}
for (const [status, alternatives] of successAlternatives.entries()) {
decodeMap[status] = makeResponseDecoder(alternatives);
}
// encoders
const encodeParams = UndefinedOr.map(endpoint.params, Schema.encodeUnknownEffect);
const payloadSchemas = HttpApiEndpoint.getPayloadSchemas(endpoint);
const encodePayload = Arr.isArrayNonEmpty(payloadSchemas) ? HttpMethod.hasBody(endpoint.method) ? Schema.encodeUnknownEffect(getEncodePayloadSchema(payloadSchemas, endpoint.method)) : Schema.encodeUnknownEffect(Schema.Union(payloadSchemas)) : undefined;
const encodeHeaders = UndefinedOr.map(endpoint.headers, Schema.encodeUnknownEffect);
const encodeQuery = UndefinedOr.map(endpoint.query, Schema.encodeUnknownEffect);
const middlewareKeys = Array.from(onEndpointOptions.middleware, tag => `${tag.key}/Client`);
const endpointFn = Effect.fnUntraced(function* (request) {
let httpRequest = HttpClientRequest.make(endpoint.method)(endpoint.path);
if (request !== undefined) {
// params
if (encodeParams !== undefined) {
const params = yield* encodeParams(request.params);
httpRequest = HttpClientRequest.setUrl(httpRequest, makeUrl(params));
}
// payload
if (encodePayload !== undefined) {
if (HttpMethod.hasBody(endpoint.method)) {
if (request.payload instanceof FormData) {
httpRequest = HttpClientRequest.bodyFormData(httpRequest, request.payload);
} else {
const body = yield* encodePayload(request.payload);
httpRequest = HttpClientRequest.setBody(httpRequest, body);
}
} else {
const urlParams = yield* encodePayload(request.payload);
httpRequest = HttpClientRequest.appendUrlParams(httpRequest, urlParams);
}
}
// headers
if (encodeHeaders !== undefined) {
const headers = yield* encodeHeaders(request.headers);
httpRequest = HttpClientRequest.setHeaders(httpRequest, headers);
}
// query
if (encodeQuery !== undefined) {
const query = yield* encodeQuery(request.query);
httpRequest = HttpClientRequest.appendUrlParams(httpRequest, query);
}
}
const response = yield* executeMiddleware(group, endpoint, httpRequest, middlewareKeys, middlewareKeys.length - 1);
if (request?.responseMode === "response-only") {
return response;
}
const value = yield* options.transformResponse === undefined ? decodeResponse(response) : options.transformResponse(decodeResponse(response));
return request?.responseMode === "decoded-and-response" ? [value, response] : value;
});
options.onEndpoint({
...onEndpointOptions,
endpointFn
});
}
});
});
/**
* Constructs a type-safe client for an HTTP API using the `HttpClient` service,
* endpoint schemas, middleware, and optional client or response transformations.
*
* @category constructors
* @since 4.0.0
*/
export const make = (api, options) => Effect.flatMap(HttpClient.HttpClient, httpClient => makeWith(api, {
...options,
httpClient: options?.transformClient ? options.transformClient(httpClient) : httpClient
}));
/**
* Constructs a type-safe client for an HTTP API from the supplied `HttpClient`,
* using the API metadata to encode requests, execute middleware, and decode
* responses.
*
* @category constructors
* @since 4.0.0
*/
export const makeWith = (api, options) => {
const client = {};
return makeClient(api, {
...options,
onGroup({
group
}) {
if (group.topLevel) return;
client[group.identifier] = {};
},
onEndpoint({
endpoint,
endpointFn,
group
}) {
;
(group.topLevel ? client : client[group.identifier])[endpoint.name] = endpointFn;
}
}).pipe(Effect.as(client));
};
/**
* Builds a typed client object for a single API group from the supplied
* `HttpClient`, filtering the API to that group.
*
* @category constructors
* @since 4.0.0
*/
export const group = (api, options) => {
const client = {};
return makeClient(api, {
...options,
predicate: ({
group
}) => group.identifier === options.group,
onEndpoint({
endpoint,
endpointFn
}) {
client[endpoint.name] = endpointFn;
}
}).pipe(Effect.map(() => client));
};
/**
* Builds the typed client method for one endpoint in one API group, using the
* supplied `HttpClient` and endpoint metadata.
*
* @category constructors
* @since 4.0.0
*/
export const endpoint = (api, options) => {
let client = undefined;
return makeClient(api, {
...options,
predicate: ({
endpoint,
group
}) => group.identifier === options.group && endpoint.name === options.endpoint,
onEndpoint({
endpointFn
}) {
client = endpointFn;
}
}).pipe(Effect.map(() => client));
};
/**
* Creates a type-safe URL builder that mirrors `HttpApiClient.make`.
*
* **Example** (Building typed URLs)
*
* ```ts
* import { Schema } from "effect"
* import { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi"
*
* const Api = HttpApi.make("Api").add(
* HttpApiGroup.make("users").add(
* HttpApiEndpoint.get("getUser", "/users/:id", {
* params: { id: Schema.String }
* })
* )
* )
*
* const buildUrl = HttpApiClient.urlBuilder(Api, {
* baseUrl: "https://api.example.com"
* })
*
* buildUrl.users.getUser({
* params: { id: "123" }
* })
* //=> "https://api.example.com/users/123"
* ```
*
* @category constructors
* @since 4.0.0
*/
export const urlBuilder = (api, options) => {
const builder = {};
HttpApi.reflect(api, {
onGroup({
group
}) {
if (group.topLevel) return;
builder[group.identifier] = {};
},
onEndpoint({
group,
endpoint
}) {
const makeUrl = compilePath(endpoint.path);
const encodeParams = endpoint.params === undefined ? undefined : Schema.encodeSync(endpoint.params);
const encodeQuery = endpoint.query === undefined ? undefined : Schema.encodeSync(endpoint.query);
const endpointBuilder = request => {
const params = request?.params;
const path = params === undefined ? endpoint.path : makeUrl(encodeParams === undefined ? params : encodeParams(params));
const queryInput = request?.query === undefined ? undefined : encodeQuery === undefined ? request.query : encodeQuery(request.query);
const query = queryInput === undefined ? "" : UrlParams.toString(UrlParams.fromInput(queryInput));
const url = query === "" ? path : `${path}?${query}`;
return options?.baseUrl === undefined ? url : new URL(url, options.baseUrl.toString()).toString();
};
(group.topLevel ? builder : builder[group.identifier])[endpoint.name] = endpointBuilder;
}
});
return builder;
};
// ----------------------------------------------------------------------------
const paramsRegExp = /(\/?):(\w+)(\?)?/g;
const compilePath = path => {
if (!paramsRegExp.test(path)) {
return _ => path;
}
paramsRegExp.lastIndex = 0;
return params => {
paramsRegExp.lastIndex = 0;
return path.replace(paramsRegExp, (_, slash, key, optional) => {
const value = params[key];
if (value === undefined) {
if (optional !== undefined) {
return "";
}
throw new Error(`Missing path parameter: ${key}`);
}
return `${slash}${encodeURIComponent(value)}`;
});
};
};
function schemasToResponse(schemas) {
const codec = toCodecArrayBuffer(schemas);
const decode = Schema.decodeEffect(codec);
return response => Effect.flatMap(response.arrayBuffer, decode);
}
function addResponseAlternative(map, status, contentType, decode) {
const normalizedContentType = normalizeContentType(contentType);
const alternatives = map.get(status);
if (alternatives === undefined) {
map.set(status, [{
contentType: normalizedContentType,
decode
}]);
} else {
alternatives.push({
contentType: normalizedContentType,
decode
});
}
}
function makeResponseDecoder(alternatives) {
const first = alternatives[0];
if (alternatives.length === 1 && first !== undefined) {
return first.decode;
}
return response => {
const contentType = normalizeContentType(response.headers["content-type"] ?? "");
const alternative = alternatives.find(alternative => alternative.contentType === contentType);
return alternative === undefined ? failUnsupportedContentType(response, contentType, alternatives) : alternative.decode(response);
};
}
function groupSchemasByContentType(schemas) {
const grouped = new Map();
for (const schema of schemas) {
const contentType = HttpApiSchema.getResponseEncoding(schema.ast).contentType;
const existing = grouped.get(contentType);
if (existing === undefined) {
grouped.set(contentType, [schema]);
} else {
existing.push(schema);
}
}
return grouped;
}
function normalizeContentType(contentType) {
const normalized = contentType.toLowerCase().trim();
const index = normalized.indexOf(";");
return index === -1 ? normalized : normalized.slice(0, index).trim();
}
function failUnsupportedContentType(response, contentType, alternatives) {
const expected = Array.from(new Set(alternatives.map(alternative => alternative.contentType))).join(", ");
return Effect.fail(new HttpClientError.HttpClientError({
reason: new HttpClientError.DecodeError({
request: response.request,
response,
description: `Unsupported response content-type for status ${response.status}: ${contentType || "<missing>"}. Expected one of: ${expected}`
})
}));
}
const reservedStreamFailureEvent = "effect/httpapi/stream/failure";
function getStreamSuccessSchemas(endpoint) {
const schemas = [];
for (const schema of endpoint.success) {
if (HttpApiSchema.isStreamSchema(schema)) {
schemas.push(schema);
}
}
return schemas;
}
function streamToResponse(streamSchema) {
return response => Effect.map(Effect.context(), context => Stream.provideContext(HttpApiSchema.isStreamUint8Array(streamSchema) ? response.stream : decodeSseStream(response.stream, streamSchema), context));
}
function decodeSseStream(stream, declaration) {
const Event = Schema.Union([declaration.events, Schema.Struct({
event: Schema.Literal(reservedStreamFailureEvent),
data: Schema.fromJsonString(Schema.toCodecJson(Schema.Cause(declaration.error, Schema.Defect())))
})]);
const events = Stream.transformPull(stream.pipe(Stream.decodeText, Stream.pipeThroughChannel(Sse.decodeSchema(Event))), pull => Effect.sync(() => {
let failureCause = undefined;
return Effect.suspend(() => {
if (failureCause) {
return Effect.failCause(failureCause);
}
return Effect.flatMap(pull, events => {
for (let i = 0; i < events.length; i++) {
const event = events[i];
if (event.event === reservedStreamFailureEvent) {
if (i === 0) {
return Effect.failCause(event.data);
}
failureCause = event.data;
events = events.slice(0, i);
break;
}
}
return Effect.succeed(events);
});
});
}));
if (declaration.sseMode === "data") {
return Stream.map(events, event => event.data);
}
return events;
}
const ArrayBuffer = /*#__PURE__*/Schema.instanceOf(globalThis.ArrayBuffer, {
expected: "ArrayBuffer"
});
// _tag: Uint8Array
const Uint8ArrayFromArrayBuffer = /*#__PURE__*/ArrayBuffer.pipe(/*#__PURE__*/Schema.decodeTo(Schema.Uint8Array, /*#__PURE__*/SchemaTransformation.transform({
decode(fromA) {
return new Uint8Array(fromA);
},
encode(arr) {
return arr.byteLength === arr.buffer.byteLength ? arr.buffer : arr.buffer.slice(arr.byteOffset, arr.byteOffset + arr.byteLength);
}
})));
// _tag: Text
const StringFromArrayBuffer = /*#__PURE__*/ArrayBuffer.pipe(/*#__PURE__*/Schema.decodeTo(Schema.String, /*#__PURE__*/SchemaTransformation.transform({
decode(fromA) {
return new TextDecoder().decode(fromA);
},
encode(toI) {
const arr = new TextEncoder().encode(toI);
return arr.byteLength === arr.buffer.byteLength ? arr.buffer : arr.buffer.slice(arr.byteOffset, arr.byteOffset + arr.byteLength);
}
})));
// _tag: Json
const UnknownFromArrayBuffer = /*#__PURE__*/StringFromArrayBuffer.pipe(/*#__PURE__*/Schema.decodeTo(/*#__PURE__*/Schema.Union([
/*#__PURE__*/
// Handle No Content
Schema.Literal("").pipe(/*#__PURE__*/Schema.decodeTo(Schema.Undefined, /*#__PURE__*/SchemaTransformation.transform({
decode: () => undefined,
encode: () => ""
}))), Schema.UnknownFromJsonString])));
function toCodecArrayBuffer(schemas) {
return Schema.Union(schemas.map(onSchema));
function onSchema(schema) {
const encoding = HttpApiSchema.getResponseEncoding(schema.ast);
switch (encoding._tag) {
case "Json":
{
// handle json codecs that transform void schemas to null
const encodedIsNull = SchemaAST.isNull(SchemaAST.toEncoded(schema.ast));
return UnknownFromArrayBuffer.pipe(Schema.decodeTo(schema, encodedIsNull ? SchemaTransformation.transform({
decode: a => a === undefined ? null : a,
encode: a => a === null ? undefined : a
}) : undefined));
}
case "FormUrlEncoded":
return StringFromArrayBuffer.pipe(Schema.decodeTo(UrlParams.schemaRecord), Schema.decodeTo(schema));
case "Uint8Array":
return Uint8ArrayFromArrayBuffer.pipe(Schema.decodeTo(schema));
case "Text":
return StringFromArrayBuffer.pipe(Schema.decodeTo(schema));
}
}
}
const statusOrElse = response => Effect.fail(new HttpClientError.HttpClientError({
reason: new HttpClientError.DecodeError({
request: response.request,
response
})
}));
const $HttpBody = /*#__PURE__*/Schema.declare(HttpBody.isHttpBody);
function getEncodePayloadSchema(schemas, method) {
return Schema.Union(schemas.map(s => getEncodePayloadSchemaFromBody(s, method)));
}
const bodyFromPayloadCache = /*#__PURE__*/new WeakMap();
function getEncodePayloadSchemaFromBody(schema, method) {
const ast = schema.ast;
const cached = bodyFromPayloadCache.get(ast);
if (cached !== undefined) {
return cached;
}
const encoding = HttpApiSchema.getPayloadEncoding(ast, method);
const out = $HttpBody.pipe(Schema.decodeTo(schema, SchemaTransformation.transformOrFail({
decode(httpBody) {
return Effect.fail(new SchemaIssue.Forbidden(Option.some(httpBody), {
message: "Encode only schema"
}));
},
encode(t) {
switch (encoding._tag) {
case "Multipart":
return Effect.fail(new SchemaIssue.Forbidden(Option.some(t), {
message: "Payload must be a FormData"
}));
case "Json":
{
try {
const body = JSON.stringify(t);
return Effect.succeed(HttpBody.text(body, encoding.contentType));
} catch (error) {
return Effect.fail(new SchemaIssue.InvalidValue(Option.some(t), {
message: globalThis.String(error)
}));
}
}
case "Text":
{
if (typeof t !== "string") {
return Effect.fail(new SchemaIssue.InvalidValue(Option.some(t), {
message: "Expected a string"
}));
}
return Effect.succeed(HttpBody.text(t, encoding.contentType));
}
case "FormUrlEncoded":
{
if (!Predicate.isObject(t)) {
return Effect.fail(new SchemaIssue.InvalidValue(Option.some(t), {
message: "Expected a record"
}));
}
return Effect.succeed(HttpBody.urlParams(UrlParams.fromInput(t)));
}
case "Uint8Array":
{
if (!(t instanceof Uint8Array)) {
return Effect.fail(new SchemaIssue.InvalidValue(Option.some(t), {
message: "Expected a Uint8Array"
}));
}
return Effect.succeed(HttpBody.uint8Array(t, encoding.contentType));
}
}
}
})));
bodyFromPayloadCache.set(ast, out);
return out;
}
//# sourceMappingURL=HttpApiClient.js.map

Xet Storage Details

Size:
20.7 kB
·
Xet hash:
22e4364fab2e6c9ca8013fdcce14f42fbeedfa4c88ed9ef52b518a69e6086f40

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.