1/**
2 * Builds HTTP response text from template literals.
3 *
4 * Template interpolations can be plain values, optional values, effects, or
5 * streams. The resulting effect or stream keeps the errors and service
6 * requirements from any effectful interpolations, which lets response helpers
7 * assemble dynamic text without losing type information.
8 *
9 * @stability unstable
10 * @since 4.0.0
11 */
12import * as Effect from "../Effect.ts"
13import * as Exit from "../Exit.ts"
14import * as Option from "../Option.ts"
15import * as Stream from "../Stream.ts"
16
17/**
18 * Primitive value that can be interpolated into an HTTP template.
19 *
20 * @stability unstable
21 * @category models
22 * @since 4.0.0
23 */
24export type PrimitiveValue = string | number | bigint | boolean | null | undefined
25
26/**
27 * Primitive template interpolation value.
28 *
29 * **Details**
30 *
31 * Arrays are rendered by converting each element to a string and concatenating the
32 * results.
33 *
34 * @stability unstable
35 * @category models
36 * @since 4.0.0
37 */
38export type Primitive = PrimitiveValue | ReadonlyArray<PrimitiveValue>
39
40/**
41 * Value accepted by the string template constructor.
42 *
43 * **Details**
44 *
45 * Interpolations can be primitive values, optional primitive values, or effects
46 * that produce primitive values.
47 *
48 * @stability unstable
49 * @category models
50 * @since 4.0.0
51 */
52export type Interpolated =
53 | Primitive
54 | Option.Option<Primitive>
55 | Effect.Effect<Primitive, any, any>
56
57/**
58 * Value accepted by the streaming template constructor.
59 *
60 * **Details**
61 *
62 * In addition to normal interpolations, stream interpolations can emit primitive
63 * values over time.
64 *
65 * @stability unstable
66 * @category models
67 * @since 4.0.0
68 */
69export type InterpolatedWithStream = Interpolated | Stream.Stream<Primitive, any, any>
70
71/**
72 * Namespace containing type-level helpers for template interpolations.
73 *
74 * @stability unstable
75 * @since 4.0.0
76 */
77export declare namespace Interpolated {
78 /**
79 * Extracts the required context from an effect or stream interpolation.
80 *
81 * **Details**
82 *
83 * Plain values and `Option` interpolations contribute no context.
84 *
85 * @stability unstable
86 * @category models
87 * @since 4.0.0
88 */
89 export type Context<A> = A extends infer T ? T extends Option.Option<infer _> ? never
90 : T extends Effect.Effect<infer _A, infer _E, infer R> ? R
91 : T extends Stream.Stream<infer _A, infer _E, infer R> ? R
92 : never
93 : never
94
95 /**
96 * Extracts the error type from an effect or stream interpolation.
97 *
98 * **Details**
99 *
100 * Plain values and `Option` interpolations contribute no error type.
101 *
102 * @stability unstable
103 * @category models
104 * @since 4.0.0
105 */
106 export type Error<A> = A extends infer T ? T extends Option.Option<infer _> ? never
107 : T extends Stream.Stream<infer _A, infer E, infer _R> ? E
108 : T extends Effect.Effect<infer _A, infer E, infer _R> ? E
109 : never
110 : never
111}
112
113/**
114 * Creates an effectful string from a template literal.
115 *
116 * **Details**
117 *
118 * Primitive and `Option` interpolations are rendered immediately. Effect
119 * interpolations are evaluated and rendered before the final string is produced.
120 *
121 * @stability unstable
122 * @category constructors
123 * @since 4.0.0
124 */
125export function make<A extends ReadonlyArray<Interpolated>>(
126 strings: TemplateStringsArray,
127 ...args: A
128): Effect.Effect<
129 string,
130 Interpolated.Error<A[number]>,
131 Interpolated.Context<A[number]>
132> {
133 const argsLength = args.length
134 const values = new Array<string>(argsLength)
135 const effects: Array<
136 [index: number, effect: Effect.Effect<Primitive, any, any>]
137 > = []
138
139 for (let i = 0; i < argsLength; i++) {
140 const arg = args[i]
141
142 if (Option.isOption(arg)) {
143 values[i] = arg._tag === "Some" ? primitiveToString(arg.value) : ""
144 } else if (isSuccess(arg)) {
145 values[i] = primitiveToString(arg.value)
146 } else if (Effect.isEffect(arg)) {
147 effects.push([i, arg])
148 } else {
149 values[i] = primitiveToString(arg)
150 }
151 }
152
153 if (effects.length === 0) {
154 return Effect.succeed(consolidate(strings, values))
155 }
156
157 return Effect.map(
158 Effect.forEach(
159 effects,
160 ([index, effect]) =>
161 Effect.tap(effect, (value) =>
162 Effect.sync(() => {
163 values[index] = primitiveToString(value)
164 })),
165 {
166 concurrency: "unbounded",
167 discard: true
168 }
169 ),
170 (_) => consolidate(strings, values)
171 )
172}
173
174/**
175 * Creates a stream of strings from a template literal.
176 *
177 * **Details**
178 *
179 * Static text is emitted with interpolated values. Effect interpolations are
180 * evaluated as stream chunks, and stream interpolations are flattened into the
181 * output.
182 *
183 * @stability unstable
184 * @category constructors
185 * @since 4.0.0
186 */
187export function stream<A extends ReadonlyArray<InterpolatedWithStream>>(
188 strings: TemplateStringsArray,
189 ...args: A
190): Stream.Stream<
191 string,
192 Interpolated.Error<A[number]>,
193 Interpolated.Context<A[number]>
194> {
195 const chunks: Array<string | Stream.Stream<string, any, any> | Effect.Effect<string, any, any>> = []
196 let buffer = ""
197
198 for (let i = 0, len = args.length; i < len; i++) {
199 buffer += strings[i]
200 const arg = args[i]
201 if (Option.isOption(arg)) {
202 buffer += arg._tag === "Some" ? primitiveToString(arg.value) : ""
203 } else if (isSuccess(arg)) {
204 buffer += primitiveToString(arg.value)
205 } else if (Effect.isEffect(arg)) {
206 if (buffer.length > 0) {
207 chunks.push(buffer)
208 buffer = ""
209 }
210 chunks.push(Effect.map(arg, primitiveToString))
211 } else if (Stream.isStream(arg)) {
212 if (buffer.length > 0) {
213 chunks.push(buffer)
214 buffer = ""
215 }
216 chunks.push(Stream.map(arg, primitiveToString))
217 } else {
218 buffer += primitiveToString(arg)
219 }
220 }
221
222 buffer += strings[strings.length - 1]
223 if (buffer.length > 0) {
224 chunks.push(buffer)
225 buffer = ""
226 }
227
228 return Stream.fromIterable(chunks).pipe(
229 Stream.mapEffect(
230 (chunk) => Effect.isEffect(chunk) ? chunk : Effect.succeed(chunk),
231 { concurrency: "unbounded" }
232 ),
233 Stream.flatMap((chunk) => typeof chunk === "string" ? Stream.succeed(chunk) : chunk)
234 )
235}
236
237function primitiveToString(value: Primitive): string {
238 if (Array.isArray(value)) {
239 return value.map(primitiveToString).join("")
240 }
241
242 switch (typeof value) {
243 case "string": {
244 return value
245 }
246 case "number":
247 case "bigint": {
248 return value.toString()
249 }
250 case "boolean": {
251 return value ? "true" : "false"
252 }
253 default: {
254 return ""
255 }
256 }
257}
258
259function consolidate(
260 strings: ReadonlyArray<string>,
261 values: ReadonlyArray<string>
262): string {
263 let out = ""
264 for (let i = 0, len = values.length; i < len; i++) {
265 out += strings[i]
266 out += values[i]
267 }
268 return out + strings[strings.length - 1]
269}
270
271function isSuccess(u: unknown): u is Exit.Success<Primitive, never> {
272 return Exit.isExit(u) && u._tag === "Success"
273}