1/**
2 * Defines named groups of HTTP API endpoints.
3 *
4 * A group collects endpoints that belong to the same resource or feature area
5 * inside an `HttpApi`. Builders, generated clients, URL builders, and OpenAPI
6 * generation read the same group value, including its identifier, endpoints,
7 * annotations, and `topLevel` flag. This module includes helpers for creating
8 * groups, adding endpoints, prefixing paths, applying middleware, annotating
9 * groups or endpoints, and deriving builder or client types.
10 *
11 * @stability unstable
12 * @since 4.0.0
13 */
14import type { NonEmptyReadonlyArray } from "../Array.ts"
15import * as Context from "../Context.ts"
16import type { PathInput } from "../http/HttpRouter.ts"
17import * as InternalRecord from "../internal/record.ts"
18import { type Pipeable, pipeArguments } from "../Pipeable.ts"
19import * as Predicate from "../Predicate.ts"
20import * as Record from "../Record.ts"
21import type * as HttpApiEndpoint from "./HttpApiEndpoint.ts"
22import type * as HttpApiMiddleware from "./HttpApiMiddleware.ts"
23
24const TypeId = "~effect/http-api/HttpApiGroup"
25
26/**
27 * Returns `true` when a value is an `HttpApiGroup`, narrowing the value to the
28 * group interface.
29 *
30 * @stability unstable
31 * @category guards
32 * @since 4.0.0
33 */
34export const isHttpApiGroup = (u: unknown): u is Top => Predicate.hasProperty(u, TypeId)
35
36/**
37 * Endpoints indexed by their identifier.
38 */
39type EndpointMap<Endpoints extends HttpApiEndpoint.Constraint> = {
40 readonly [Endpoint in Endpoints as HttpApiEndpoint.Identifier<Endpoint>]: Endpoint
41}
42
43/**
44 * An `HttpApiGroup` is a named collection of `HttpApiEndpoint`s that represents
45 * a portion of your domain.
46 *
47 * **Details**
48 *
49 * Endpoint implementations can be provided later with `HttpApiBuilder.group`.
50 *
51 * @stability unstable
52 * @category models
53 * @since 4.0.0
54 */
55export interface HttpApiGroup<
56 out Id extends string,
57 in out Endpoints extends HttpApiEndpoint.Constraint = never,
58 out TopLevel extends boolean = false
59> extends Pipeable {
60 new(_: never): {}
61 readonly [TypeId]: typeof TypeId
62 /**
63 * Stable group identifier. This field intentionally is not named `name`
64 * because `HttpApiGroup` values can be extended as classes, where `name`
65 * would collide with JavaScript's built-in `Function.name`.
66 */
67 readonly identifier: Id
68 readonly key: string
69 readonly topLevel: TopLevel
70 readonly endpoints: EndpointMap<Endpoints>
71 readonly annotations: Context.Context<never>
72
73 /**
74 * Add an `HttpApiEndpoint` to an `HttpApiGroup`.
75 */
76 add<const A extends NonEmptyReadonlyArray<HttpApiEndpoint.Constraint>>(
77 ...endpoints: A
78 ): HttpApiGroup<Id, Endpoints | A[number], TopLevel>
79
80 /**
81 * Add a path prefix to all endpoints in an `HttpApiGroup`. Note that this will only
82 * add the prefix to the endpoints before this api is called.
83 */
84 prefix<const Prefix extends PathInput>(
85 prefix: Prefix
86 ): HttpApiGroup<Id, HttpApiEndpoint.AddPrefix<Endpoints, Prefix>, TopLevel>
87
88 /**
89 * Adds an `HttpApiMiddleware` to every endpoint currently in the group.
90 *
91 * **Gotchas**
92 *
93 * Endpoints added after this method is called do not have the middleware
94 * applied.
95 */
96 middleware<I extends HttpApiMiddleware.AnyId, S>(middleware: Context.Key<I, S>): HttpApiGroup<
97 Id,
98 HttpApiEndpoint.AddMiddleware<Endpoints, I>,
99 TopLevel
100 >
101
102 /**
103 * Merge the annotations of an `HttpApiGroup` with the provided annotations.
104 */
105 annotateMerge<I>(annotations: Context.Context<I>): HttpApiGroup<Id, Endpoints, TopLevel>
106
107 /**
108 * Add an annotation to an `HttpApiGroup`.
109 */
110 annotate<I, S>(key: Context.Key<I, S>, value: S): HttpApiGroup<Id, Endpoints, TopLevel>
111
112 /**
113 * Merges the provided context into every endpoint currently in the group.
114 *
115 * **Gotchas**
116 *
117 * Endpoints added after this method is called do not have these annotations.
118 */
119 annotateEndpointsMerge<I>(annotations: Context.Context<I>): HttpApiGroup<Id, Endpoints, TopLevel>
120
121 /**
122 * Adds an annotation to every endpoint currently in the group.
123 *
124 * **Gotchas**
125 *
126 * Endpoints added after this method is called do not have this annotation.
127 */
128 annotateEndpoints<I, S>(key: Context.Key<I, S>, value: S): HttpApiGroup<Id, Endpoints, TopLevel>
129}
130
131/**
132 * Type-level service produced by the layer that implements one group of an HTTP
133 * API.
134 *
135 * **Details**
136 *
137 * `HttpApiBuilder.group` provides this service, and `HttpApiBuilder.layer`
138 * requires one service for each group in the API. The type carries both the API
139 * id and the group identifier so the relationship between an API and its
140 * implemented groups is checked at compile time.
141 *
142 * @stability unstable
143 * @category services
144 * @since 4.0.0
145 */
146export interface Service<ApiId extends string, Identifier extends string> {
147 readonly _: unique symbol
148 readonly apiId: ApiId
149 readonly identifier: Identifier
150}
151
152/**
153 * Derives the group implementation service required for each group in an HTTP
154 * API.
155 *
156 * **Details**
157 *
158 * When given an API id and a group or union of groups, this type maps each group
159 * to the `Service` identity that must be provided by `HttpApiBuilder.group`.
160 *
161 * @stability unstable
162 * @category utility types
163 * @since 4.0.0
164 */
165export type ToService<ApiId extends string, Group extends Constraint> = Group extends Constraint ?
166 Service<ApiId, Group["identifier"]>
167 : never
168
169/**
170 * A widened `HttpApiGroup` type used when the concrete group identifier,
171 * endpoints, and top-level flag are not needed.
172 *
173 * @stability unstable
174 * @category models
175 * @since 4.0.0
176 */
177export interface Constraint {
178 readonly [TypeId]: typeof TypeId
179 readonly identifier: string
180 readonly key: string
181 readonly endpoints: Record.ReadonlyRecord<string, HttpApiEndpoint.Constraint>
182}
183
184/**
185 * A widened group type that preserves concrete runtime properties such as
186 * identifier, key, top-level status, endpoints, and annotations.
187 *
188 * @stability unstable
189 * @category models
190 * @since 4.0.0
191 */
192export interface Top extends HttpApiGroup<string, HttpApiEndpoint.Top, boolean> {}
193
194/**
195 * Selects the group with the specified identifier from a union of groups.
196 *
197 * @stability unstable
198 * @category utility types
199 * @since 4.0.0
200 */
201export type WithIdentifier<Group, Identifier extends string> = Extract<Group, { readonly identifier: Identifier }>
202
203/**
204 * Extracts the identifier literal from an `HttpApiGroup`.
205 *
206 * @stability unstable
207 * @category utility types
208 * @since 4.0.0
209 */
210export type Identifier<Group> = Group extends Constraint ? Group["identifier"] : never
211
212/**
213 * Extracts the endpoint union contained in an `HttpApiGroup`.
214 *
215 * @stability unstable
216 * @category utility types
217 * @since 4.0.0
218 */
219export type Endpoints<Group> = Group extends HttpApiGroup<infer _Identifier, infer _Endpoints, infer _TopLevel> ?
220 _Endpoints
221 : never
222
223/**
224 * Computes the services required to encode error responses for every endpoint in a
225 * group.
226 *
227 * @stability unstable
228 * @category utility types
229 * @since 4.0.0
230 */
231export type ErrorServicesEncode<Group> = HttpApiEndpoint.ErrorServicesEncode<Endpoints<Group>>
232
233/**
234 * Computes the services required to decode error responses for every endpoint in a
235 * group.
236 *
237 * @stability unstable
238 * @category utility types
239 * @since 4.0.0
240 */
241export type ErrorServicesDecode<Group> = HttpApiEndpoint.ErrorServicesDecode<Endpoints<Group>>
242
243/**
244 * Computes the middleware error union for every endpoint in a group.
245 *
246 * @stability unstable
247 * @category utility types
248 * @since 4.0.0
249 */
250export type MiddlewareError<Group> = HttpApiEndpoint.MiddlewareError<Endpoints<Group>>
251
252/**
253 * Computes the services provided by middleware attached to any endpoint in a
254 * group.
255 *
256 * @stability unstable
257 * @category utility types
258 * @since 4.0.0
259 */
260export type MiddlewareProvides<Group> = HttpApiEndpoint.MiddlewareProvides<Endpoints<Group>>
261
262/**
263 * Computes the client-side middleware services required by endpoints in a group.
264 *
265 * @stability unstable
266 * @category utility types
267 * @since 4.0.0
268 */
269export type MiddlewareClient<Group> = HttpApiEndpoint.MiddlewareClient<Endpoints<Group>>
270
271/**
272 * Extracts the runtime services required by middleware attached to the endpoints in an `HttpApiGroup`.
273 *
274 * @stability unstable
275 * @category utility types
276 * @since 4.0.0
277 */
278export type MiddlewareServices<Group> = HttpApiEndpoint.MiddlewareServices<Endpoints<Group>>
279
280/**
281 * Extracts the endpoint union from the group with the specified identifier.
282 *
283 * @stability unstable
284 * @category utility types
285 * @since 4.0.0
286 */
287export type EndpointsWithIdentifier<Group extends Constraint, Identifier extends string> = Endpoints<
288 WithIdentifier<Group, Identifier>
289>
290
291/**
292 * Computes the schema encoding and decoding services required by clients for all endpoints in a group.
293 *
294 * @stability unstable
295 * @category utility types
296 * @since 4.0.0
297 */
298export type ClientServices<Group> = Group extends HttpApiGroup<infer _Identifier, infer _Endpoints, infer _TopLevel> ?
299 HttpApiEndpoint.ClientServices<_Endpoints>
300 : never
301
302/**
303 * Returns the type of a group after adding the supplied path prefix to each endpoint in the group.
304 *
305 * @stability unstable
306 * @category utility types
307 * @since 4.0.0
308 */
309export type AddPrefix<Group, Prefix extends PathInput> = Group extends
310 HttpApiGroup<infer _Identifier, infer _Endpoints, infer _TopLevel> ?
311 HttpApiGroup<_Identifier, HttpApiEndpoint.AddPrefix<_Endpoints, Prefix>, _TopLevel>
312 : never
313
314/**
315 * Returns the type of a group after applying a middleware identifier to every endpoint in the group.
316 *
317 * @stability unstable
318 * @category utility types
319 * @since 4.0.0
320 */
321export type AddMiddleware<Group, Id extends HttpApiMiddleware.AnyId> = Group extends
322 HttpApiGroup<infer _Identifier, infer _Endpoints, infer _TopLevel> ?
323 HttpApiGroup<_Identifier, HttpApiEndpoint.AddMiddleware<_Endpoints, Id>, _TopLevel>
324 : never
325
326const Proto = {
327 [TypeId]: TypeId,
328 add(this: Top, ...toAdd: NonEmptyReadonlyArray<HttpApiEndpoint.Top>) {
329 const endpoints = { ...this.endpoints }
330 for (const endpoint of toAdd) {
331 InternalRecord.assignProperty(endpoints, endpoint.identifier, endpoint)
332 }
333 return makeProto({
334 ...optionsFromGroup(this),
335 endpoints
336 })
337 },
338 prefix(this: Top, prefix: PathInput) {
339 return makeProto({
340 ...optionsFromGroup(this),
341 endpoints: Record.map(this.endpoints, (endpoint) => endpoint.prefix(prefix))
342 })
343 },
344 middleware(this: Top, middleware: HttpApiMiddleware.AnyService) {
345 return makeProto({
346 ...optionsFromGroup(this),
347 endpoints: Record.map(this.endpoints, (endpoint) => endpoint.middleware(middleware as any))
348 })
349 },
350 annotateMerge<I>(this: Top, annotations: Context.Context<I>) {
351 return makeProto({
352 ...optionsFromGroup(this),
353 annotations: Context.merge(this.annotations, annotations)
354 })
355 },
356 annotate<I, S>(this: Top, annotation: Context.Key<I, S>, value: S) {
357 return makeProto({
358 ...optionsFromGroup(this),
359 annotations: Context.add(this.annotations, annotation, value)
360 })
361 },
362 annotateEndpointsMerge<I>(this: Top, annotations: Context.Context<I>) {
363 return makeProto({
364 ...optionsFromGroup(this),
365 endpoints: Record.map(this.endpoints, (endpoint) => endpoint.annotateMerge(annotations))
366 })
367 },
368 annotateEndpoints<I, S>(this: Top, annotation: Context.Key<I, S>, value: S) {
369 return makeProto({
370 ...optionsFromGroup(this),
371 endpoints: Record.map(this.endpoints, (endpoint) => endpoint.annotate(annotation, value))
372 })
373 },
374 pipe() {
375 return pipeArguments(this, arguments)
376 }
377}
378
379const optionsFromGroup = (group: Top) => ({
380 identifier: group.identifier,
381 topLevel: group.topLevel,
382 endpoints: group.endpoints,
383 annotations: group.annotations
384})
385
386const makeProto = <
387 Id extends string,
388 Endpoints extends HttpApiEndpoint.Constraint,
389 TopLevel extends (true | false)
390>(options: {
391 readonly identifier: Id
392 readonly topLevel: TopLevel
393 readonly endpoints: Record.ReadonlyRecord<string, HttpApiEndpoint.Constraint>
394 readonly annotations: Context.Context<never>
395}): HttpApiGroup<Id, Endpoints, TopLevel> => {
396 function HttpApiGroup() {}
397 Object.setPrototypeOf(HttpApiGroup, Proto)
398 HttpApiGroup.key = `effect/http-api/HttpApiGroup/${options.identifier}`
399 return Object.assign(HttpApiGroup, options) as any
400}
401
402/**
403 * Creates an empty `HttpApiGroup` with the supplied identifier.
404 *
405 * **Details**
406 *
407 * Add endpoints with `add`, provide implementations with `HttpApiBuilder.group`,
408 * and set `topLevel` when the generated client should expose endpoint methods
409 * directly instead of nesting them under the group identifier.
410 *
411 * @stability unstable
412 * @category constructors
413 * @since 4.0.0
414 */
415export const make = <const Id extends string, const TopLevel extends boolean = false>(identifier: Id, options?: {
416 readonly topLevel?: TopLevel | undefined
417}): HttpApiGroup<Id, never, TopLevel> =>
418 makeProto({
419 identifier,
420 topLevel: (options?.topLevel ?? false) as TopLevel,
421 endpoints: {},
422 annotations: Context.empty()
423 })