@effect/platform 0.62.4 → 0.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/HttpApi/package.json +6 -0
  2. package/HttpApiBuilder/package.json +6 -0
  3. package/HttpApiClient/package.json +6 -0
  4. package/HttpApiEndpoint/package.json +6 -0
  5. package/HttpApiError/package.json +6 -0
  6. package/HttpApiGroup/package.json +6 -0
  7. package/HttpApiSchema/package.json +6 -0
  8. package/HttpApiSecurity/package.json +6 -0
  9. package/HttpApiSwagger/package.json +6 -0
  10. package/OpenApi/package.json +6 -0
  11. package/README.md +863 -302
  12. package/dist/cjs/HttpApi.js +168 -0
  13. package/dist/cjs/HttpApi.js.map +1 -0
  14. package/dist/cjs/HttpApiBuilder.js +471 -0
  15. package/dist/cjs/HttpApiBuilder.js.map +1 -0
  16. package/dist/cjs/HttpApiClient.js +134 -0
  17. package/dist/cjs/HttpApiClient.js.map +1 -0
  18. package/dist/cjs/HttpApiEndpoint.js +181 -0
  19. package/dist/cjs/HttpApiEndpoint.js.map +1 -0
  20. package/dist/cjs/HttpApiError.js +69 -0
  21. package/dist/cjs/HttpApiError.js.map +1 -0
  22. package/dist/cjs/HttpApiGroup.js +151 -0
  23. package/dist/cjs/HttpApiGroup.js.map +1 -0
  24. package/dist/cjs/HttpApiSchema.js +241 -0
  25. package/dist/cjs/HttpApiSchema.js.map +1 -0
  26. package/dist/cjs/HttpApiSecurity.js +83 -0
  27. package/dist/cjs/HttpApiSecurity.js.map +1 -0
  28. package/dist/cjs/HttpApiSwagger.js +50 -0
  29. package/dist/cjs/HttpApiSwagger.js.map +1 -0
  30. package/dist/cjs/HttpMethod.js +1 -1
  31. package/dist/cjs/HttpMethod.js.map +1 -1
  32. package/dist/cjs/HttpRouter.js +6 -1
  33. package/dist/cjs/HttpRouter.js.map +1 -1
  34. package/dist/cjs/OpenApi.js +317 -0
  35. package/dist/cjs/OpenApi.js.map +1 -0
  36. package/dist/cjs/index.js +21 -1
  37. package/dist/cjs/internal/apiSwagger.js +2 -0
  38. package/dist/cjs/internal/apiSwagger.js.map +1 -0
  39. package/dist/cjs/internal/httpRouter.js +6 -1
  40. package/dist/cjs/internal/httpRouter.js.map +1 -1
  41. package/dist/cjs/internal/multipart.js +5 -1
  42. package/dist/cjs/internal/multipart.js.map +1 -1
  43. package/dist/dts/HttpApi.d.ts +156 -0
  44. package/dist/dts/HttpApi.d.ts.map +1 -0
  45. package/dist/dts/HttpApiBuilder.d.ts +296 -0
  46. package/dist/dts/HttpApiBuilder.d.ts.map +1 -0
  47. package/dist/dts/HttpApiClient.d.ts +31 -0
  48. package/dist/dts/HttpApiClient.d.ts.map +1 -0
  49. package/dist/dts/HttpApiEndpoint.d.ts +350 -0
  50. package/dist/dts/HttpApiEndpoint.d.ts.map +1 -0
  51. package/dist/dts/HttpApiError.d.ts +70 -0
  52. package/dist/dts/HttpApiError.d.ts.map +1 -0
  53. package/dist/dts/HttpApiGroup.d.ts +196 -0
  54. package/dist/dts/HttpApiGroup.d.ts.map +1 -0
  55. package/dist/dts/HttpApiSchema.d.ts +223 -0
  56. package/dist/dts/HttpApiSchema.d.ts.map +1 -0
  57. package/dist/dts/HttpApiSecurity.d.ts +122 -0
  58. package/dist/dts/HttpApiSecurity.d.ts.map +1 -0
  59. package/dist/dts/HttpApiSwagger.d.ts +10 -0
  60. package/dist/dts/HttpApiSwagger.d.ts.map +1 -0
  61. package/dist/dts/HttpMethod.d.ts +16 -0
  62. package/dist/dts/HttpMethod.d.ts.map +1 -1
  63. package/dist/dts/HttpRouter.d.ts +8 -0
  64. package/dist/dts/HttpRouter.d.ts.map +1 -1
  65. package/dist/dts/HttpServerResponse.d.ts +3 -3
  66. package/dist/dts/HttpServerResponse.d.ts.map +1 -1
  67. package/dist/dts/OpenApi.d.ts +348 -0
  68. package/dist/dts/OpenApi.d.ts.map +1 -0
  69. package/dist/dts/index.d.ts +40 -0
  70. package/dist/dts/index.d.ts.map +1 -1
  71. package/dist/dts/internal/apiSwagger.d.ts +2 -0
  72. package/dist/dts/internal/apiSwagger.d.ts.map +1 -0
  73. package/dist/dts/internal/httpRouter.d.ts.map +1 -1
  74. package/dist/esm/HttpApi.js +157 -0
  75. package/dist/esm/HttpApi.js.map +1 -0
  76. package/dist/esm/HttpApiBuilder.js +447 -0
  77. package/dist/esm/HttpApiBuilder.js.map +1 -0
  78. package/dist/esm/HttpApiClient.js +124 -0
  79. package/dist/esm/HttpApiClient.js.map +1 -0
  80. package/dist/esm/HttpApiEndpoint.js +169 -0
  81. package/dist/esm/HttpApiEndpoint.js.map +1 -0
  82. package/dist/esm/HttpApiError.js +59 -0
  83. package/dist/esm/HttpApiError.js.map +1 -0
  84. package/dist/esm/HttpApiGroup.js +140 -0
  85. package/dist/esm/HttpApiGroup.js.map +1 -0
  86. package/dist/esm/HttpApiSchema.js +217 -0
  87. package/dist/esm/HttpApiSchema.js.map +1 -0
  88. package/dist/esm/HttpApiSecurity.js +73 -0
  89. package/dist/esm/HttpApiSecurity.js.map +1 -0
  90. package/dist/esm/HttpApiSwagger.js +40 -0
  91. package/dist/esm/HttpApiSwagger.js.map +1 -0
  92. package/dist/esm/HttpMethod.js +1 -1
  93. package/dist/esm/HttpMethod.js.map +1 -1
  94. package/dist/esm/HttpRouter.js +5 -0
  95. package/dist/esm/HttpRouter.js.map +1 -1
  96. package/dist/esm/OpenApi.js +299 -0
  97. package/dist/esm/OpenApi.js.map +1 -0
  98. package/dist/esm/index.js +40 -0
  99. package/dist/esm/index.js.map +1 -1
  100. package/dist/esm/internal/apiSwagger.js +2 -0
  101. package/dist/esm/internal/apiSwagger.js.map +1 -0
  102. package/dist/esm/internal/httpRouter.js +5 -0
  103. package/dist/esm/internal/httpRouter.js.map +1 -1
  104. package/dist/esm/internal/multipart.js +5 -1
  105. package/dist/esm/internal/multipart.js.map +1 -1
  106. package/package.json +83 -3
  107. package/src/HttpApi.ts +342 -0
  108. package/src/HttpApiBuilder.ts +869 -0
  109. package/src/HttpApiClient.ts +228 -0
  110. package/src/HttpApiEndpoint.ts +818 -0
  111. package/src/HttpApiError.ts +113 -0
  112. package/src/HttpApiGroup.ts +365 -0
  113. package/src/HttpApiSchema.ts +384 -0
  114. package/src/HttpApiSecurity.ts +169 -0
  115. package/src/HttpApiSwagger.ts +46 -0
  116. package/src/HttpMethod.ts +19 -1
  117. package/src/HttpRouter.ts +9 -0
  118. package/src/HttpServerResponse.ts +3 -3
  119. package/src/OpenApi.ts +665 -0
  120. package/src/index.ts +50 -0
  121. package/src/internal/apiSwagger.ts +7 -0
  122. package/src/internal/httpRouter.ts +9 -0
  123. package/src/internal/multipart.ts +5 -1
package/README.md CHANGED
@@ -8,416 +8,573 @@ This package empowers you to perform various operations, such as:
8
8
 
9
9
  | **Operation** | **Description** |
10
10
  | -------------- | ------------------------------------------------------------------------------------------------ |
11
- | Terminal | Reading and writing from/to standard input/output |
12
- | Command | Creating and running a command with the specified process name and an optional list of arguments |
13
- | FileSystem | Reading and writing from/to the file system |
11
+ | HTTP API | Declarative HTTP API servers & clients |
14
12
  | HTTP Client | Sending HTTP requests and receiving responses |
15
13
  | HTTP Server | Creating HTTP servers to handle incoming requests |
16
14
  | HTTP Router | Routing HTTP requests to specific handlers |
15
+ | Terminal | Reading and writing from/to standard input/output |
16
+ | Command | Creating and running a command with the specified process name and an optional list of arguments |
17
+ | FileSystem | Reading and writing from/to the file system |
17
18
  | KeyValueStore | Storing and retrieving key-value pairs |
18
19
  | PlatformLogger | Creating a logger that writes to a specified file from another string logger |
19
20
 
20
21
  By utilizing `@effect/platform`, you can write code that remains platform-agnostic, ensuring compatibility across different environments.
21
22
 
22
- # Terminal
23
+ # HTTP API
23
24
 
24
- The `@effect/platform/Terminal` module exports a single `Terminal` tag, which serves as the entry point to reading from and writing to standard input and standard output.
25
+ ## Overview
25
26
 
26
- ## Writing to standard output
27
+ The `HttpApi` family of modules provide a declarative way to define HTTP APIs.
28
+ You can create an API by combining multiple endpoints, each with its own set of
29
+ schemas that define the request and response types.
27
30
 
28
- ```ts
29
- import { Terminal } from "@effect/platform"
30
- import { NodeRuntime, NodeTerminal } from "@effect/platform-node"
31
- import { Effect } from "effect"
31
+ After you have defined your API, you can use it to implement a server or derive
32
+ a client that can interact with the server.
32
33
 
33
- // const displayMessage: Effect.Effect<void, PlatformError, Terminal.Terminal>
34
- const displayMessage = Effect.gen(function* (_) {
35
- const terminal = yield* _(Terminal.Terminal)
36
- yield* _(terminal.display("a message\n"))
37
- })
34
+ ## Defining an API
38
35
 
39
- NodeRuntime.runMain(displayMessage.pipe(Effect.provide(NodeTerminal.layer)))
40
- // Output: "a message"
36
+ To define an API, you need to create a set of endpoints. Each endpoint is
37
+ defined by a path, a method, and a set of schemas that define the request and
38
+ response types.
39
+
40
+ Each set of endpoints is added to an `HttpApiGroup`, which can be combined with
41
+ other groups to create a complete API.
42
+
43
+ ### Your first `HttpApiGroup`
44
+
45
+ Let's define a simple CRUD API for managing users. First, we need to make an
46
+ `HttpApiGroup` that contains our endpoints.
47
+
48
+ ```ts
49
+ import { HttpApiEndpoint, HttpApiGroup } from "@effect/platform"
50
+ import { Schema } from "@effect/schema"
51
+
52
+ // Our domain "User" Schema
53
+ class User extends Schema.Class<User>("User")({
54
+ id: Schema.Number,
55
+ name: Schema.String,
56
+ createdAt: Schema.DateTimeUtc
57
+ }) {}
58
+
59
+ const usersApi = HttpApiGroup.make("users").pipe(
60
+ HttpApiGroup.add(
61
+ // each endpoint has a name and a path
62
+ HttpApiEndpoint.get("findById", "/users/:id").pipe(
63
+ // the endpoint can have a Schema for a successful response
64
+ HttpApiEndpoint.setSuccess(User),
65
+ // and here is a Schema for the path parameters
66
+ HttpApiEndpoint.setPath(
67
+ Schema.Struct({
68
+ id: Schema.NumberFromString
69
+ })
70
+ )
71
+ )
72
+ ),
73
+ HttpApiGroup.add(
74
+ HttpApiEndpoint.post("create", "/users").pipe(
75
+ HttpApiEndpoint.setSuccess(User),
76
+ // and here is a Schema for the request payload / body
77
+ //
78
+ // this is a POST request, so the payload is in the body
79
+ // but for a GET request, the payload would be in the URL search params
80
+ HttpApiEndpoint.setPayload(
81
+ Schema.Struct({
82
+ name: Schema.String
83
+ })
84
+ )
85
+ )
86
+ ),
87
+ // by default, the endpoint will respond with a 204 No Content
88
+ HttpApiGroup.add(HttpApiEndpoint.del("delete", "/users/:id")),
89
+ HttpApiGroup.add(
90
+ HttpApiEndpoint.patch("update", "/users/:id").pipe(
91
+ HttpApiEndpoint.setSuccess(User),
92
+ HttpApiEndpoint.setPayload(
93
+ Schema.Struct({
94
+ name: Schema.String
95
+ })
96
+ )
97
+ )
98
+ )
99
+ )
41
100
  ```
42
101
 
43
- ## Reading from standard input
102
+ You can also extend the `HttpApiGroup` with a class to gain an opaque type.
103
+ We will use this API style in the following examples:
44
104
 
45
105
  ```ts
46
- import { Terminal } from "@effect/platform"
47
- import { NodeRuntime, NodeTerminal } from "@effect/platform-node"
48
- import { Console, Effect } from "effect"
106
+ class UsersApi extends HttpApiGroup.make("users").pipe(
107
+ HttpApiGroup.add(
108
+ HttpApiEndpoint.get("findById", "/users/:id")
109
+ // ... same as above
110
+ )
111
+ ) {}
112
+ ```
49
113
 
50
- // const readLine: Effect.Effect<void, Terminal.QuitException, Terminal.Terminal>
51
- const readLine = Effect.gen(function* (_) {
52
- const terminal = yield* _(Terminal.Terminal)
53
- const input = yield* _(terminal.readLine)
54
- yield* _(Console.log(`input: ${input}`))
55
- })
114
+ ### Creating the top level `HttpApi`
56
115
 
57
- NodeRuntime.runMain(readLine.pipe(Effect.provide(NodeTerminal.layer)))
58
- // Input: "hello"
59
- // Output: "input: hello"
116
+ Once you have defined your groups, you can combine them into a single `HttpApi`.
117
+
118
+ ```ts
119
+ import { HttpApi } from "@effect/platform"
120
+
121
+ class MyApi extends HttpApi.empty.pipe(HttpApi.addGroup(UsersApi)) {}
60
122
  ```
61
123
 
62
- These simple examples illustrate how to utilize the `Terminal` module for handling standard input and output in your programs. Let's use this knowledge to build a number guessing game:
124
+ Or with the non-opaque style:
63
125
 
64
126
  ```ts
65
- import { Terminal } from "@effect/platform"
66
- import type { PlatformError } from "@effect/platform/Error"
67
- import { Effect, Option, Random } from "effect"
127
+ const api = HttpApi.empty.pipe(HttpApi.addGroup(usersApi))
128
+ ```
68
129
 
69
- export const secret = Random.nextIntBetween(1, 100)
130
+ ### Adding OpenApi annotations
70
131
 
71
- const parseGuess = (input: string) => {
72
- const n = parseInt(input, 10)
73
- return isNaN(n) || n < 1 || n > 100 ? Option.none() : Option.some(n)
74
- }
132
+ You can add OpenApi annotations to your API by using the `OpenApi` module.
75
133
 
76
- const display = (message: string) =>
77
- Effect.gen(function* (_) {
78
- const terminal = yield* _(Terminal.Terminal)
79
- yield* _(terminal.display(`${message}\n`))
134
+ Let's add a title to our `UsersApi` group:
135
+
136
+ ```ts
137
+ import { OpenApi } from "@effect/platform"
138
+
139
+ class UsersApi extends HttpApiGroup.make("users").pipe(
140
+ HttpApiGroup.add(
141
+ HttpApiEndpoint.get("findById", "/users/:id")
142
+ // ... same as above
143
+ ),
144
+ // add an OpenApi title & description
145
+ OpenApi.annotate({
146
+ title: "Users API",
147
+ description: "API for managing users"
80
148
  })
149
+ ) {}
150
+ ```
81
151
 
82
- const prompt = Effect.gen(function* (_) {
83
- const terminal = yield* _(Terminal.Terminal)
84
- yield* _(terminal.display("Enter a guess: "))
85
- return yield* _(terminal.readLine)
86
- })
152
+ Now when you generate OpenApi documentation, the title and description will be
153
+ included.
87
154
 
88
- const answer: Effect.Effect<
89
- number,
90
- Terminal.QuitException | PlatformError,
91
- Terminal.Terminal
92
- > = Effect.gen(function* (_) {
93
- const input = yield* _(prompt)
94
- const guess = parseGuess(input)
95
- if (Option.isNone(guess)) {
96
- yield* _(display("You must enter an integer from 1 to 100"))
97
- return yield* _(answer)
98
- }
99
- return guess.value
100
- })
155
+ You can also add OpenApi annotations to the top-level `HttpApi`:
101
156
 
102
- const check = <A, E, R>(
103
- secret: number,
104
- guess: number,
105
- ok: Effect.Effect<A, E, R>,
106
- ko: Effect.Effect<A, E, R>
107
- ): Effect.Effect<A, E | PlatformError, R | Terminal.Terminal> =>
108
- Effect.gen(function* (_) {
109
- if (guess > secret) {
110
- yield* _(display("Too high"))
111
- return yield* _(ko)
112
- } else if (guess < secret) {
113
- yield* _(display("Too low"))
114
- return yield* _(ko)
115
- } else {
116
- return yield* _(ok)
117
- }
157
+ ```ts
158
+ class MyApi extends HttpApi.empty.pipe(
159
+ HttpApi.addGroup(UsersApi),
160
+ OpenApi.annotate({
161
+ title: "My API",
162
+ description: "My awesome API"
118
163
  })
164
+ ) {}
165
+ ```
119
166
 
120
- const end = display("You guessed it!")
167
+ ### Adding errors
121
168
 
122
- const loop = (
123
- secret: number
124
- ): Effect.Effect<
125
- void,
126
- Terminal.QuitException | PlatformError,
127
- Terminal.Terminal
128
- > =>
129
- Effect.gen(function* (_) {
130
- const guess = yield* _(answer)
131
- return yield* _(
132
- check(
133
- secret,
134
- guess,
135
- end,
136
- Effect.suspend(() => loop(secret))
137
- )
138
- )
139
- })
169
+ You can add error responses to your endpoints using the following apis:
140
170
 
141
- export const game = Effect.gen(function* (_) {
142
- yield* _(
143
- display(
144
- "We have selected a random number between 1 and 100. See if you can guess it in 10 turns or fewer. We'll tell you if your guess was too high or too low."
145
- )
146
- )
147
- yield* _(loop(yield* _(secret)))
148
- })
149
- ```
171
+ - `HttpApiEndpoint.addError` - add an error response for a single endpoint
172
+ - `HttpApiGroup.addError` - add an error response for all endpoints in a group
173
+ - `HttpApi.addError` - add an error response for all endpoints in the api
150
174
 
151
- Let's run the game in Node.js:
175
+ The group & api level errors are useful for adding common error responses that
176
+ can be used in middleware.
177
+
178
+ Here is an example of adding a 404 error to the `UsersApi` group:
152
179
 
153
180
  ```ts
154
- import { NodeRuntime, NodeTerminal } from "@effect/platform-node"
155
- import * as Effect from "effect/Effect"
156
- import { game } from "./game.js"
181
+ // define the error schemas
182
+ class UserNotFound extends Schema.TaggedError<UserNotFound>()(
183
+ "UserNotFound",
184
+ {}
185
+ ) {}
157
186
 
158
- NodeRuntime.runMain(game.pipe(Effect.provide(NodeTerminal.layer)))
187
+ class Unauthorized extends Schema.TaggedError<Unauthorized>()(
188
+ "Unauthorized",
189
+ {}
190
+ ) {}
191
+
192
+ class UsersApi extends HttpApiGroup.make("users").pipe(
193
+ HttpApiGroup.add(
194
+ HttpApiEndpoint.get("findById", "/users/:id").pipe(
195
+ // here we are adding our error response
196
+ HttpApiEndpoint.addError(UserNotFound, { status: 404 }),
197
+ HttpApiEndpoint.setSuccess(User),
198
+ HttpApiEndpoint.setPath(Schema.Struct({ id: Schema.NumberFromString }))
199
+ )
200
+ ),
201
+ // or we could add an error to the group
202
+ HttpApiGroup.addError(Unauthorized, { status: 401 })
203
+ ) {}
159
204
  ```
160
205
 
161
- Let's run the game in Bun:
206
+ It is worth noting that you can add multiple error responses to an endpoint,
207
+ just by calling `HttpApiEndpoint.addError` multiple times.
208
+
209
+ ### Multipart requests
210
+
211
+ If you need to handle file uploads, you can use the `HttpApiSchema.Multipart`
212
+ api to flag a `HttpApiEndpoint` payload schema as a multipart request.
213
+
214
+ You can then use the schemas from the `Multipart` module to define the expected
215
+ shape of the multipart request.
162
216
 
163
217
  ```ts
164
- import { BunRuntime, BunTerminal } from "@effect/platform-bun"
165
- import * as Effect from "effect/Effect"
166
- import { game } from "./game.js"
218
+ import { HttpApiSchema, Multipart } from "@effect/platform"
167
219
 
168
- BunRuntime.runMain(game.pipe(Effect.provide(BunTerminal.layer)))
220
+ class UsersApi extends HttpApiGroup.make("users").pipe(
221
+ HttpApiGroup.add(
222
+ HttpApiEndpoint.post("upload", "/users/upload").pipe(
223
+ HttpApiEndpoint.setPayload(
224
+ HttpApiSchema.Multipart(
225
+ Schema.Struct({
226
+ // add a "files" field to the schema
227
+ files: Multipart.FilesSchema
228
+ })
229
+ )
230
+ )
231
+ )
232
+ )
233
+ ) {}
169
234
  ```
170
235
 
171
- # Command
236
+ ### Adding security annotations
172
237
 
173
- As an example of using the `@effect/platform/Command` module, let's see how to run the TypeScript compiler `tsc`:
238
+ The `HttpApiSecurity` module provides a way to add security annotations to your
239
+ API.
174
240
 
175
- ```ts
176
- import { Command, CommandExecutor } from "@effect/platform"
177
- import {
178
- NodeCommandExecutor,
179
- NodeFileSystem,
180
- NodeRuntime
181
- } from "@effect/platform-node"
182
- import { Effect } from "effect"
241
+ It offers the following authorization types:
183
242
 
184
- // const program: Effect.Effect<string, PlatformError, CommandExecutor.CommandExecutor>
185
- const program = Effect.gen(function* (_) {
186
- const executor = yield* _(CommandExecutor.CommandExecutor)
243
+ - `HttpApiSecurity.apiKey` - API key authorization through headers, query
244
+ parameters, or cookies.
245
+ - `HttpApiSecurity.basicAuth` - HTTP Basic authentication.
246
+ - `HttpApiSecurity.bearerAuth` - Bearer token authentication.
187
247
 
188
- // Creating a command to run the TypeScript compiler
189
- const command = Command.make("tsc", "--noEmit")
190
- console.log("Running tsc...")
248
+ You can annotate your API with these security types using the
249
+ `OpenApi.annotate` api as before.
191
250
 
192
- // Executing the command and capturing the output
193
- const output = yield* _(executor.string(command))
194
- console.log(output)
195
- return output
251
+ ```ts
252
+ import { HttpApiSecurity } from "@effect/platform"
253
+
254
+ const security = HttpApiSecurity.apiKey({
255
+ in: "cookie",
256
+ key: "token"
196
257
  })
197
258
 
198
- // Running the program with the necessary runtime and executor layers
199
- NodeRuntime.runMain(
200
- program.pipe(
201
- Effect.provide(NodeCommandExecutor.layer),
202
- Effect.provide(NodeFileSystem.layer)
203
- )
204
- )
259
+ class UsersApi extends HttpApiGroup.make("users").pipe(
260
+ HttpApiGroup.add(
261
+ HttpApiEndpoint.get("findById", "/users/:id").pipe(
262
+ // add the security annotation to the endpoint
263
+ OpenApi.annotate({ security })
264
+ )
265
+ ),
266
+ // or at the group level
267
+ OpenApi.annotate({ security }),
268
+
269
+ // or just for the endpoints above this line
270
+ HttpApiGroup.annotateEndpoints(OpenApi.Security, security),
271
+ // this endpoint will not have the security annotation
272
+ HttpApiGroup.add(HttpApiEndpoint.get("list", "/users"))
273
+ ) {}
205
274
  ```
206
275
 
207
- ## Obtaining Information About the Running Process
276
+ ### Changing the response encoding
208
277
 
209
- Here, we'll explore how to retrieve information about a running process.
278
+ By default, the response is encoded as JSON. You can change the encoding using
279
+ the `HttpApiSchema.withEncoding` api.
280
+
281
+ Here is an example of changing the encoding to text/csv:
210
282
 
211
283
  ```ts
212
- import { Command, CommandExecutor } from "@effect/platform"
213
- import {
214
- NodeCommandExecutor,
215
- NodeFileSystem,
216
- NodeRuntime
217
- } from "@effect/platform-node"
218
- import { Effect, Stream, String } from "effect"
284
+ class UsersApi extends HttpApiGroup.make("users").pipe(
285
+ HttpApiGroup.add(
286
+ HttpApiEndpoint.get("csv", "/users/csv").pipe(
287
+ HttpApiEndpoint.setSuccess(
288
+ Schema.String.pipe(
289
+ HttpApiSchema.withEncoding({
290
+ kind: "Text",
291
+ contentType: "text/csv"
292
+ })
293
+ )
294
+ )
295
+ )
296
+ )
297
+ ) {}
298
+ ```
219
299
 
220
- const runString = <E, R>(
221
- stream: Stream.Stream<Uint8Array, E, R>
222
- ): Effect.Effect<string, E, R> =>
223
- stream.pipe(Stream.decodeText(), Stream.runFold(String.empty, String.concat))
300
+ ## Implementing a server
224
301
 
225
- const program = Effect.gen(function* (_) {
226
- const executor = yield* _(CommandExecutor.CommandExecutor)
302
+ Now that you have defined your API, you can implement a server that serves the
303
+ endpoints.
227
304
 
228
- const command = Command.make("ls")
305
+ The `HttpApiBuilder` module provides all the apis you need to implement your
306
+ server.
229
307
 
230
- const [exitCode, stdout, stderr] = yield* _(
231
- // Start running the command and return a handle to the running process.
232
- executor.start(command),
233
- Effect.flatMap((process) =>
234
- Effect.all(
235
- [
236
- // Waits for the process to exit and returns the ExitCode of the command that was run.
237
- process.exitCode,
238
- // The standard output stream of the process.
239
- runString(process.stdout),
240
- // The standard error stream of the process.
241
- runString(process.stderr)
242
- ],
243
- { concurrency: 3 }
308
+ ### Implementing a `HttpApiGroup`
309
+
310
+ First up, let's implement an `UsersApi` group with a single `findById` endpoint.
311
+
312
+ The `HttpApiBuilder.group` api takes the `HttpApi` definition, the group name,
313
+ and a function that adds the handlers required for the group.
314
+
315
+ Each endpoint is implemented using the `HttpApiBuilder.handle` api.
316
+
317
+ ```ts
318
+ import {
319
+ HttpApi,
320
+ HttpApiBuilder,
321
+ HttpApiEndpoint,
322
+ HttpApiGroup
323
+ } from "@effect/platform"
324
+ import { Schema } from "@effect/schema"
325
+ import { DateTime, Effect } from "effect"
326
+
327
+ // here is our api definition
328
+ class User extends Schema.Class<User>("User")({
329
+ id: Schema.Number,
330
+ name: Schema.String,
331
+ createdAt: Schema.DateTimeUtc
332
+ }) {}
333
+
334
+ class UsersApi extends HttpApiGroup.make("users").pipe(
335
+ HttpApiGroup.add(
336
+ HttpApiEndpoint.get("findById", "/users/:id").pipe(
337
+ HttpApiEndpoint.setSuccess(User),
338
+ HttpApiEndpoint.setPath(
339
+ Schema.Struct({
340
+ id: Schema.NumberFromString
341
+ })
244
342
  )
245
343
  )
246
344
  )
247
- console.log({ exitCode, stdout, stderr })
248
- })
249
-
250
- NodeRuntime.runMain(
251
- Effect.scoped(program).pipe(
252
- Effect.provide(NodeCommandExecutor.layer),
253
- Effect.provide(NodeFileSystem.layer)
345
+ ) {}
346
+
347
+ class MyApi extends HttpApi.empty.pipe(HttpApi.addGroup(UsersApi)) {}
348
+
349
+ // --------------------------------------------
350
+ // Implementation
351
+ // --------------------------------------------
352
+
353
+ // the `HttpApiBuilder.group` api returns a `Layer`
354
+ const UsersApiLive: Layer.Layer<HttpApiGroup.HttpApiGroup.Service<"users">> =
355
+ HttpApiBuilder.group(MyApi, "users", (handlers) =>
356
+ handlers.pipe(
357
+ // the parameters & payload are passed to the handler function.
358
+ HttpApiBuilder.handle("findById", ({ path: { id } }) =>
359
+ Effect.succeed(
360
+ new User({
361
+ id,
362
+ name: "John Doe",
363
+ createdAt: DateTime.unsafeNow()
364
+ })
365
+ )
366
+ )
367
+ )
254
368
  )
255
- )
256
369
  ```
257
370
 
258
- ## Running a Platform Command with stdout Streamed to process.stdout
371
+ ### Using services inside a `HttpApiGroup`
259
372
 
260
- To run a command (for example `cat`) and stream its `stdout` to `process.stdout` follow these steps:
373
+ If you need to use services inside your handlers, you can return an
374
+ `Effect` from the `HttpApiBuilder.group` api.
261
375
 
262
376
  ```ts
263
- import { Command } from "@effect/platform"
264
- import { NodeContext, NodeRuntime } from "@effect/platform-node"
265
- import { Effect } from "effect"
266
-
267
- // Create a command to run `cat` on a file and inherit stdout
268
- const program = Command.make("cat", "./some-file.txt").pipe(
269
- Command.stdout("inherit"),
270
- Command.exitCode
377
+ class UsersRepository extends Context.Tag("UsersRepository")<
378
+ UsersRepository,
379
+ {
380
+ readonly findById: (id: number) => Effect.Effect<User>
381
+ }
382
+ >() {}
383
+
384
+ // the dependencies will show up in the resulting `Layer`
385
+ const UsersApiLive: Layer.Layer<
386
+ HttpApiGroup.HttpApiGroup.Service<"users">,
387
+ never,
388
+ UsersRepository
389
+ > = HttpApiBuilder.group(MyApi, "users", (handlers) =>
390
+ // we can return an Effect that creates our handlers
391
+ Effect.gen(function* () {
392
+ const repository = yield* UsersRepository
393
+ return handlers.pipe(
394
+ HttpApiBuilder.handle("findById", ({ path: { id } }) =>
395
+ repository.findById(id)
396
+ )
397
+ )
398
+ })
271
399
  )
272
-
273
- // Run the command using NodeRuntime with the NodeContext layer
274
- NodeRuntime.runMain(program.pipe(Effect.provide(NodeContext.layer)))
275
400
  ```
276
401
 
277
- # FileSystem
278
-
279
- The `@effect/platform/FileSystem` module provides a single `FileSystem` tag, which acts as the gateway for interacting with the filesystem.
280
-
281
- Here's a list of operations that can be performed using the `FileSystem` tag:
402
+ ### Implementing a `HttpApi`
282
403
 
283
- | **Name** | **Arguments** | **Return** | **Description** |
284
- | --------------------------- | ---------------------------------------------------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
285
- | **access** | `path: string`, `options?: AccessFileOptions` | `Effect<void, PlatformError>` | Check if a file can be accessed. You can optionally specify the level of access to check for. |
286
- | **copy** | `fromPath: string`, `toPath: string`, `options?: CopyOptions` | `Effect<void, PlatformError>` | Copy a file or directory from `fromPath` to `toPath`. Equivalent to `cp -r`. |
287
- | **copyFile** | `fromPath: string`, `toPath: string` | `Effect<void, PlatformError>` | Copy a file from `fromPath` to `toPath`. |
288
- | **chmod** | `path: string`, `mode: number` | `Effect<void, PlatformError>` | Change the permissions of a file. |
289
- | **chown** | `path: string`, `uid: number`, `gid: number` | `Effect<void, PlatformError>` | Change the owner and group of a file. |
290
- | **exists** | `path: string` | `Effect<boolean, PlatformError>` | Check if a path exists. |
291
- | **link** | `fromPath: string`, `toPath: string` | `Effect<void, PlatformError>` | Create a hard link from `fromPath` to `toPath`. |
292
- | **makeDirectory** | `path: string`, `options?: MakeDirectoryOptions` | `Effect<void, PlatformError>` | Create a directory at `path`. You can optionally specify the mode and whether to recursively create nested directories. |
293
- | **makeTempDirectory** | `options?: MakeTempDirectoryOptions` | `Effect<string, PlatformError>` | Create a temporary directory. By default, the directory will be created inside the system's default temporary directory. |
294
- | **makeTempDirectoryScoped** | `options?: MakeTempDirectoryOptions` | `Effect<string, PlatformError, Scope>` | Create a temporary directory inside a scope. Functionally equivalent to `makeTempDirectory`, but the directory will be automatically deleted when the scope is closed. |
295
- | **makeTempFile** | `options?: MakeTempFileOptions` | `Effect<string, PlatformError>` | Create a temporary file. The directory creation is functionally equivalent to `makeTempDirectory`. The file name will be a randomly generated string. |
296
- | **makeTempFileScoped** | `options?: MakeTempFileOptions` | `Effect<string, PlatformError, Scope>` | Create a temporary file inside a scope. Functionally equivalent to `makeTempFile`, but the file will be automatically deleted when the scope is closed. |
297
- | **open** | `path: string`, `options?: OpenFileOptions` | `Effect<File, PlatformError, Scope>` | Open a file at `path` with the specified `options`. The file handle will be automatically closed when the scope is closed. |
298
- | **readDirectory** | `path: string`, `options?: ReadDirectoryOptions` | `Effect<Array<string>, PlatformError>` | List the contents of a directory. You can recursively list the contents of nested directories by setting the `recursive` option. |
299
- | **readFile** | `path: string` | `Effect<Uint8Array, PlatformError>` | Read the contents of a file. |
300
- | **readFileString** | `path: string`, `encoding?: string` | `Effect<string, PlatformError>` | Read the contents of a file as a string. |
301
- | **readLink** | `path: string` | `Effect<string, PlatformError>` | Read the destination of a symbolic link. |
302
- | **realPath** | `path: string` | `Effect<string, PlatformError>` | Resolve a path to its canonicalized absolute pathname. |
303
- | **remove** | `path: string`, `options?: RemoveOptions` | `Effect<void, PlatformError>` | Remove a file or directory. By setting the `recursive` option to `true`, you can recursively remove nested directories. |
304
- | **rename** | `oldPath: string`, `newPath: string` | `Effect<void, PlatformError>` | Rename a file or directory. |
305
- | **sink** | `path: string`, `options?: SinkOptions` | `Sink<void, Uint8Array, never, PlatformError>` | Create a writable `Sink` for the specified `path`. |
306
- | **stat** | `path: string` | `Effect<File.Info, PlatformError>` | Get information about a file at `path`. |
307
- | **stream** | `path: string`, `options?: StreamOptions` | `Stream<Uint8Array, PlatformError>` | Create a readable `Stream` for the specified `path`. |
308
- | **symlink** | `fromPath: string`, `toPath: string` | `Effect<void, PlatformError>` | Create a symbolic link from `fromPath` to `toPath`. |
309
- | **truncate** | `path: string`, `length?: SizeInput` | `Effect<void, PlatformError>` | Truncate a file to a specified length. If the `length` is not specified, the file will be truncated to length `0`. |
310
- | **utimes** | `path: string`, `atime: Date \| number`, `mtime: Date \| number` | `Effect<void, PlatformError>` | Change the file system timestamps of the file at `path`. |
311
- | **watch** | `path: string` | `Stream<WatchEvent, PlatformError>` | Watch a directory or file for changes. |
404
+ Once all your groups are implemented, you can implement the top-level `HttpApi`.
312
405
 
313
- Let's explore a simple example using `readFileString`:
406
+ This is done using the `HttpApiBuilder.api` api, and then using `Layer.provide`
407
+ to add all the group implementations.
314
408
 
315
409
  ```ts
316
- import { FileSystem } from "@effect/platform"
317
- import { NodeFileSystem, NodeRuntime } from "@effect/platform-node"
318
- import { Effect } from "effect"
410
+ const MyApiLive: Layer.Layer<HttpApi.HttpApi.Service> = HttpApiBuilder.api(
411
+ MyApi
412
+ ).pipe(Layer.provide(UsersApiLive))
413
+ ```
319
414
 
320
- // const readFileString: Effect.Effect<void, PlatformError, FileSystem.FileSystem>
321
- const readFileString = Effect.gen(function* (_) {
322
- const fs = yield* _(FileSystem.FileSystem)
415
+ ### Serving the API
323
416
 
324
- // Reading the content of the same file where this code is written
325
- const content = yield* _(fs.readFileString("./index.ts", "utf8"))
326
- console.log(content)
327
- })
417
+ Finally, you can serve the API using the `HttpApiBuilder.serve` api.
328
418
 
329
- NodeRuntime.runMain(readFileString.pipe(Effect.provide(NodeFileSystem.layer)))
330
- ```
419
+ You can also add middleware to the server using the `HttpMiddleware` module, or
420
+ use some of the middleware Layer's from the `HttpApiBuilder` module.
331
421
 
332
- # KeyValueStore
422
+ ```ts
423
+ import { HttpMiddleware, HttpServer } from "@effect/platform"
424
+ import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
425
+ import { createServer } from "node:http"
333
426
 
334
- ## Overview
427
+ // use the `HttpApiBuilder.serve` function to register our API with the HTTP
428
+ // server
429
+ const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
430
+ // Add CORS middleware
431
+ Layer.provide(HttpApiBuilder.middlewareCors()),
432
+ // Provide the API implementation
433
+ Layer.provide(MyApiLive),
434
+ // Log the address the server is listening on
435
+ HttpServer.withLogAddress,
436
+ // Provide the HTTP server implementation
437
+ Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
438
+ )
335
439
 
336
- The `KeyValueStore` module provides a robust and effectful interface for managing key-value pairs. It supports asynchronous operations, ensuring data integrity and consistency, and includes built-in implementations for in-memory, file system-based, and schema-validated stores.
440
+ // run the server
441
+ Layer.launch(HttpLive).pipe(NodeRuntime.runMain)
442
+ ```
337
443
 
338
- ## Basic Usage
444
+ ## Implementing `HttpApiSecurity`
339
445
 
340
- The `KeyValueStore` interface includes the following operations:
446
+ If you are using `HttpApiSecurity` in your API, you can use the security
447
+ definition to implement a middleware that will protect your endpoints.
341
448
 
342
- - **get**: Retrieve a value by key.
343
- - **set**: Store a key-value pair.
344
- - **remove**: Delete a key-value pair.
345
- - **clear**: Remove all key-value pairs.
346
- - **size**: Get the number of stored pairs.
347
- - **modify**: Atomically modify a value.
348
- - **has**: Check if a key exists.
349
- - **isEmpty**: Check if the store is empty.
449
+ The `HttpApiBuilder.middlewareSecurity` api will assist you in creating this
450
+ middleware.
350
451
 
351
- **Example**
452
+ Here is an example:
352
453
 
353
454
  ```ts
354
- import { KeyValueStore, layerMemory } from "@effect/platform/KeyValueStore"
355
- import { Effect } from "effect"
455
+ // our cookie security definition
456
+ const security = HttpApiSecurity.apiKey({
457
+ in: "cookie",
458
+ key: "token"
459
+ })
356
460
 
357
- const program = Effect.gen(function* () {
358
- const store = yield* KeyValueStore
359
- console.log(yield* store.size) // Outputs: 0
461
+ // the user repository service
462
+ class UsersRepository extends Context.Tag("UsersRepository")<
463
+ UsersRepository,
464
+ {
465
+ readonly findByToken: (token: Redacted.Redacted) => Effect.Effect<User>
466
+ }
467
+ >() {}
468
+
469
+ // the security middleware will supply the current user to the handlers
470
+ class CurrentUser extends Context.Tag("CurrentUser")<CurrentUser, User>() {}
471
+
472
+ // implement the middleware
473
+ const makeSecurityMiddleware: Effect.Effect<
474
+ HttpApiBuilder.SecurityMiddleware<CurrentUser>,
475
+ never,
476
+ UsersRepository
477
+ > = Effect.gen(function* () {
478
+ const repository = yield* UsersRepository
479
+ return HttpApiBuilder.middlewareSecurity(
480
+ // the security definition
481
+ security,
482
+ // the Context.Tag this middleware will provide
483
+ CurrentUser,
484
+ // the function to get the user from the token
485
+ (token) => repository.findByToken(token)
486
+ )
487
+ })
360
488
 
361
- yield* store.set("key", "value")
362
- console.log(yield* store.size) // Outputs: 1
489
+ // use the middleware
490
+ const UsersApiLive = HttpApiBuilder.group(MyApi, "users", (handlers) =>
491
+ Effect.gen(function* () {
492
+ // construct the security middleware
493
+ const securityMiddleware = yield* makeSecurityMiddleware
494
+
495
+ return handlers.pipe(
496
+ HttpApiBuilder.handle("findById", ({ path: { id } }) =>
497
+ Effect.succeed(
498
+ new User({ id, name: "John Doe", createdAt: DateTime.unsafeNow() })
499
+ )
500
+ ),
501
+ // apply the middleware to the findById endpoint
502
+ securityMiddleware
503
+ // any endpoint after this will not be protected
504
+ )
505
+ })
506
+ )
507
+ ```
363
508
 
364
- const value = yield* store.get("key")
365
- console.log(value) // Outputs: { _id: 'Option', _tag: 'Some', value: 'value' }
509
+ If you need to set the security cookie from within a handler, you can use the
510
+ `HttpApiBuilder.securitySetCookie` api.
366
511
 
367
- yield* store.remove("key")
368
- console.log(yield* store.size) // Outputs: 0
512
+ By default, the cookie will be set with the `HttpOnly` and `Secure` flags.
513
+
514
+ ```ts
515
+ const security = HttpApiSecurity.apiKey({
516
+ in: "cookie",
517
+ key: "token"
369
518
  })
370
519
 
371
- Effect.runPromise(program.pipe(Effect.provide(layerMemory)))
520
+ const UsersApiLive = HttpApiBuilder.group(MyApi, "users", (handlers) =>
521
+ handlers.pipe(
522
+ HttpApiBuilder.handle("login", () =>
523
+ // set the security cookie
524
+ HttpApiBuilder.securitySetCookie(
525
+ security,
526
+ Redacted.make("keep me secret")
527
+ )
528
+ )
529
+ )
530
+ )
372
531
  ```
373
532
 
374
- ## Built-in Implementations
375
-
376
- The module provides several built-in implementations to suit different needs:
533
+ ### Serving Swagger documentation
377
534
 
378
- - **In-Memory Store**: `layerMemory` provides a simple, in-memory key-value store, ideal for lightweight or testing scenarios.
379
- - **File System Store**: `layerFileSystem` offers a file-based store for persistent storage needs.
380
- - **Schema Store**: `layerSchema` enables schema-based validation for stored values, ensuring data integrity and type safety.
535
+ You can add Swagger documentation to your API using the `HttpApiSwagger` module.
381
536
 
382
- ## Schema Store
537
+ You just need to provide the `HttpApiSwagger.layer` to your server
538
+ implementation:
383
539
 
384
- The `SchemaStore` implementation allows you to validate and parse values according to a defined schema. This ensures that all data stored in the key-value store adheres to the specified structure, enhancing data integrity and type safety.
540
+ ```ts
541
+ import { HttpApiSwagger } from "@effect/platform"
385
542
 
386
- **Example**
543
+ const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
544
+ // add the swagger documentation layer
545
+ Layer.provide(
546
+ HttpApiSwagger.layer({
547
+ // "/docs" is the default path for the swagger documentation
548
+ path: "/docs"
549
+ })
550
+ ),
551
+ Layer.provide(HttpApiBuilder.middlewareCors()),
552
+ Layer.provide(MyApiLive),
553
+ Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
554
+ )
555
+ ```
387
556
 
388
- ```ts
389
- import { KeyValueStore, layerMemory } from "@effect/platform/KeyValueStore"
390
- import { Schema } from "@effect/schema"
391
- import { Effect } from "effect"
557
+ ## Deriving a client
392
558
 
393
- // Define a schema for the values
394
- const Person = Schema.Struct({
395
- name: Schema.String,
396
- age: Schema.Number
397
- })
559
+ Once you have defined your API, you can derive a client that can interact with
560
+ the server.
398
561
 
399
- const program = Effect.gen(function* () {
400
- const store = (yield* KeyValueStore).forSchema(Person)
562
+ The `HttpApiClient` module provides all the apis you need to derive a client.
401
563
 
402
- // Create a value that adheres to the schema
403
- const value = { name: "Alice", age: 30 }
404
- yield* store.set("user1", value)
405
- console.log(yield* store.size) // Outputs: 1
564
+ ```ts
565
+ import { HttpApiClient } from "@effect/platform"
406
566
 
407
- // Retrieve and validate the value
408
- const retrievedValue = yield* store.get("user1")
409
- console.log(retrievedValue) // Outputs: { _id: 'Option', _tag: 'Some', value: { name: 'Alice', age: 30 } }
567
+ Effect.gen(function* () {
568
+ const client = yield* HttpApiClient.make(MyApi, {
569
+ baseUrl: "http://localhost:3000"
570
+ // You can transform the HttpClient to add things like authentication
571
+ // transformClient: ....
572
+ })
573
+ const user = yield* client.users.findById({ path: { id: 1 } })
574
+ yield* Effect.log(user)
410
575
  })
411
-
412
- Effect.runPromise(program.pipe(Effect.provide(layerMemory)))
413
576
  ```
414
577
 
415
- In this example:
416
-
417
- - **Person**: Defines the structure for the values stored in the key-value store.
418
- - **store.set**: Stores a value adhering to `Person`.
419
- - **store.get**: Retrieves and validates the stored value against `Person`.
420
-
421
578
  # HTTP Client
422
579
 
423
580
  ## Overview
@@ -500,7 +657,12 @@ const myClient = HttpClient.makeDefault((req) =>
500
657
  req,
501
658
  // Simulate a response from a server
502
659
  new Response(
503
- JSON.stringify({ userId: 1, id: 1, title: "title...", body: "body..." })
660
+ JSON.stringify({
661
+ userId: 1,
662
+ id: 1,
663
+ title: "title...",
664
+ body: "body..."
665
+ })
504
666
  )
505
667
  )
506
668
  )
@@ -2183,3 +2345,402 @@ const handler = HttpApp.toWebHandler(router)
2183
2345
  const response = await handler(new Request("http://localhost:3000/foo"))
2184
2346
  console.log(await response.text()) // Output: content 2
2185
2347
  ```
2348
+
2349
+ # Terminal
2350
+
2351
+ The `@effect/platform/Terminal` module exports a single `Terminal` tag, which serves as the entry point to reading from and writing to standard input and standard output.
2352
+
2353
+ ## Writing to standard output
2354
+
2355
+ ```ts
2356
+ import { Terminal } from "@effect/platform"
2357
+ import { NodeRuntime, NodeTerminal } from "@effect/platform-node"
2358
+ import { Effect } from "effect"
2359
+
2360
+ // const displayMessage: Effect.Effect<void, PlatformError, Terminal.Terminal>
2361
+ const displayMessage = Effect.gen(function* (_) {
2362
+ const terminal = yield* _(Terminal.Terminal)
2363
+ yield* _(terminal.display("a message\n"))
2364
+ })
2365
+
2366
+ NodeRuntime.runMain(displayMessage.pipe(Effect.provide(NodeTerminal.layer)))
2367
+ // Output: "a message"
2368
+ ```
2369
+
2370
+ ## Reading from standard input
2371
+
2372
+ ```ts
2373
+ import { Terminal } from "@effect/platform"
2374
+ import { NodeRuntime, NodeTerminal } from "@effect/platform-node"
2375
+ import { Console, Effect } from "effect"
2376
+
2377
+ // const readLine: Effect.Effect<void, Terminal.QuitException, Terminal.Terminal>
2378
+ const readLine = Effect.gen(function* (_) {
2379
+ const terminal = yield* _(Terminal.Terminal)
2380
+ const input = yield* _(terminal.readLine)
2381
+ yield* _(Console.log(`input: ${input}`))
2382
+ })
2383
+
2384
+ NodeRuntime.runMain(readLine.pipe(Effect.provide(NodeTerminal.layer)))
2385
+ // Input: "hello"
2386
+ // Output: "input: hello"
2387
+ ```
2388
+
2389
+ These simple examples illustrate how to utilize the `Terminal` module for handling standard input and output in your programs. Let's use this knowledge to build a number guessing game:
2390
+
2391
+ ```ts
2392
+ import { Terminal } from "@effect/platform"
2393
+ import type { PlatformError } from "@effect/platform/Error"
2394
+ import { Effect, Option, Random } from "effect"
2395
+
2396
+ export const secret = Random.nextIntBetween(1, 100)
2397
+
2398
+ const parseGuess = (input: string) => {
2399
+ const n = parseInt(input, 10)
2400
+ return isNaN(n) || n < 1 || n > 100 ? Option.none() : Option.some(n)
2401
+ }
2402
+
2403
+ const display = (message: string) =>
2404
+ Effect.gen(function* (_) {
2405
+ const terminal = yield* _(Terminal.Terminal)
2406
+ yield* _(terminal.display(`${message}\n`))
2407
+ })
2408
+
2409
+ const prompt = Effect.gen(function* (_) {
2410
+ const terminal = yield* _(Terminal.Terminal)
2411
+ yield* _(terminal.display("Enter a guess: "))
2412
+ return yield* _(terminal.readLine)
2413
+ })
2414
+
2415
+ const answer: Effect.Effect<
2416
+ number,
2417
+ Terminal.QuitException | PlatformError,
2418
+ Terminal.Terminal
2419
+ > = Effect.gen(function* (_) {
2420
+ const input = yield* _(prompt)
2421
+ const guess = parseGuess(input)
2422
+ if (Option.isNone(guess)) {
2423
+ yield* _(display("You must enter an integer from 1 to 100"))
2424
+ return yield* _(answer)
2425
+ }
2426
+ return guess.value
2427
+ })
2428
+
2429
+ const check = <A, E, R>(
2430
+ secret: number,
2431
+ guess: number,
2432
+ ok: Effect.Effect<A, E, R>,
2433
+ ko: Effect.Effect<A, E, R>
2434
+ ): Effect.Effect<A, E | PlatformError, R | Terminal.Terminal> =>
2435
+ Effect.gen(function* (_) {
2436
+ if (guess > secret) {
2437
+ yield* _(display("Too high"))
2438
+ return yield* _(ko)
2439
+ } else if (guess < secret) {
2440
+ yield* _(display("Too low"))
2441
+ return yield* _(ko)
2442
+ } else {
2443
+ return yield* _(ok)
2444
+ }
2445
+ })
2446
+
2447
+ const end = display("You guessed it!")
2448
+
2449
+ const loop = (
2450
+ secret: number
2451
+ ): Effect.Effect<
2452
+ void,
2453
+ Terminal.QuitException | PlatformError,
2454
+ Terminal.Terminal
2455
+ > =>
2456
+ Effect.gen(function* (_) {
2457
+ const guess = yield* _(answer)
2458
+ return yield* _(
2459
+ check(
2460
+ secret,
2461
+ guess,
2462
+ end,
2463
+ Effect.suspend(() => loop(secret))
2464
+ )
2465
+ )
2466
+ })
2467
+
2468
+ export const game = Effect.gen(function* (_) {
2469
+ yield* _(
2470
+ display(
2471
+ "We have selected a random number between 1 and 100. See if you can guess it in 10 turns or fewer. We'll tell you if your guess was too high or too low."
2472
+ )
2473
+ )
2474
+ yield* _(loop(yield* _(secret)))
2475
+ })
2476
+ ```
2477
+
2478
+ Let's run the game in Node.js:
2479
+
2480
+ ```ts
2481
+ import { NodeRuntime, NodeTerminal } from "@effect/platform-node"
2482
+ import * as Effect from "effect/Effect"
2483
+ import { game } from "./game.js"
2484
+
2485
+ NodeRuntime.runMain(game.pipe(Effect.provide(NodeTerminal.layer)))
2486
+ ```
2487
+
2488
+ Let's run the game in Bun:
2489
+
2490
+ ```ts
2491
+ import { BunRuntime, BunTerminal } from "@effect/platform-bun"
2492
+ import * as Effect from "effect/Effect"
2493
+ import { game } from "./game.js"
2494
+
2495
+ BunRuntime.runMain(game.pipe(Effect.provide(BunTerminal.layer)))
2496
+ ```
2497
+
2498
+ # Command
2499
+
2500
+ As an example of using the `@effect/platform/Command` module, let's see how to run the TypeScript compiler `tsc`:
2501
+
2502
+ ```ts
2503
+ import { Command, CommandExecutor } from "@effect/platform"
2504
+ import {
2505
+ NodeCommandExecutor,
2506
+ NodeFileSystem,
2507
+ NodeRuntime
2508
+ } from "@effect/platform-node"
2509
+ import { Effect } from "effect"
2510
+
2511
+ // const program: Effect.Effect<string, PlatformError, CommandExecutor.CommandExecutor>
2512
+ const program = Effect.gen(function* (_) {
2513
+ const executor = yield* _(CommandExecutor.CommandExecutor)
2514
+
2515
+ // Creating a command to run the TypeScript compiler
2516
+ const command = Command.make("tsc", "--noEmit")
2517
+ console.log("Running tsc...")
2518
+
2519
+ // Executing the command and capturing the output
2520
+ const output = yield* _(executor.string(command))
2521
+ console.log(output)
2522
+ return output
2523
+ })
2524
+
2525
+ // Running the program with the necessary runtime and executor layers
2526
+ NodeRuntime.runMain(
2527
+ program.pipe(
2528
+ Effect.provide(NodeCommandExecutor.layer),
2529
+ Effect.provide(NodeFileSystem.layer)
2530
+ )
2531
+ )
2532
+ ```
2533
+
2534
+ ## Obtaining Information About the Running Process
2535
+
2536
+ Here, we'll explore how to retrieve information about a running process.
2537
+
2538
+ ```ts
2539
+ import { Command, CommandExecutor } from "@effect/platform"
2540
+ import {
2541
+ NodeCommandExecutor,
2542
+ NodeFileSystem,
2543
+ NodeRuntime
2544
+ } from "@effect/platform-node"
2545
+ import { Effect, Stream, String } from "effect"
2546
+
2547
+ const runString = <E, R>(
2548
+ stream: Stream.Stream<Uint8Array, E, R>
2549
+ ): Effect.Effect<string, E, R> =>
2550
+ stream.pipe(Stream.decodeText(), Stream.runFold(String.empty, String.concat))
2551
+
2552
+ const program = Effect.gen(function* (_) {
2553
+ const executor = yield* _(CommandExecutor.CommandExecutor)
2554
+
2555
+ const command = Command.make("ls")
2556
+
2557
+ const [exitCode, stdout, stderr] = yield* _(
2558
+ // Start running the command and return a handle to the running process.
2559
+ executor.start(command),
2560
+ Effect.flatMap((process) =>
2561
+ Effect.all(
2562
+ [
2563
+ // Waits for the process to exit and returns the ExitCode of the command that was run.
2564
+ process.exitCode,
2565
+ // The standard output stream of the process.
2566
+ runString(process.stdout),
2567
+ // The standard error stream of the process.
2568
+ runString(process.stderr)
2569
+ ],
2570
+ { concurrency: 3 }
2571
+ )
2572
+ )
2573
+ )
2574
+ console.log({ exitCode, stdout, stderr })
2575
+ })
2576
+
2577
+ NodeRuntime.runMain(
2578
+ Effect.scoped(program).pipe(
2579
+ Effect.provide(NodeCommandExecutor.layer),
2580
+ Effect.provide(NodeFileSystem.layer)
2581
+ )
2582
+ )
2583
+ ```
2584
+
2585
+ ## Running a Platform Command with stdout Streamed to process.stdout
2586
+
2587
+ To run a command (for example `cat`) and stream its `stdout` to `process.stdout` follow these steps:
2588
+
2589
+ ```ts
2590
+ import { Command } from "@effect/platform"
2591
+ import { NodeContext, NodeRuntime } from "@effect/platform-node"
2592
+ import { Effect } from "effect"
2593
+
2594
+ // Create a command to run `cat` on a file and inherit stdout
2595
+ const program = Command.make("cat", "./some-file.txt").pipe(
2596
+ Command.stdout("inherit"),
2597
+ Command.exitCode
2598
+ )
2599
+
2600
+ // Run the command using NodeRuntime with the NodeContext layer
2601
+ NodeRuntime.runMain(program.pipe(Effect.provide(NodeContext.layer)))
2602
+ ```
2603
+
2604
+ # FileSystem
2605
+
2606
+ The `@effect/platform/FileSystem` module provides a single `FileSystem` tag, which acts as the gateway for interacting with the filesystem.
2607
+
2608
+ Here's a list of operations that can be performed using the `FileSystem` tag:
2609
+
2610
+ | **Name** | **Arguments** | **Return** | **Description** |
2611
+ | --------------------------- | ---------------------------------------------------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2612
+ | **access** | `path: string`, `options?: AccessFileOptions` | `Effect<void, PlatformError>` | Check if a file can be accessed. You can optionally specify the level of access to check for. |
2613
+ | **copy** | `fromPath: string`, `toPath: string`, `options?: CopyOptions` | `Effect<void, PlatformError>` | Copy a file or directory from `fromPath` to `toPath`. Equivalent to `cp -r`. |
2614
+ | **copyFile** | `fromPath: string`, `toPath: string` | `Effect<void, PlatformError>` | Copy a file from `fromPath` to `toPath`. |
2615
+ | **chmod** | `path: string`, `mode: number` | `Effect<void, PlatformError>` | Change the permissions of a file. |
2616
+ | **chown** | `path: string`, `uid: number`, `gid: number` | `Effect<void, PlatformError>` | Change the owner and group of a file. |
2617
+ | **exists** | `path: string` | `Effect<boolean, PlatformError>` | Check if a path exists. |
2618
+ | **link** | `fromPath: string`, `toPath: string` | `Effect<void, PlatformError>` | Create a hard link from `fromPath` to `toPath`. |
2619
+ | **makeDirectory** | `path: string`, `options?: MakeDirectoryOptions` | `Effect<void, PlatformError>` | Create a directory at `path`. You can optionally specify the mode and whether to recursively create nested directories. |
2620
+ | **makeTempDirectory** | `options?: MakeTempDirectoryOptions` | `Effect<string, PlatformError>` | Create a temporary directory. By default, the directory will be created inside the system's default temporary directory. |
2621
+ | **makeTempDirectoryScoped** | `options?: MakeTempDirectoryOptions` | `Effect<string, PlatformError, Scope>` | Create a temporary directory inside a scope. Functionally equivalent to `makeTempDirectory`, but the directory will be automatically deleted when the scope is closed. |
2622
+ | **makeTempFile** | `options?: MakeTempFileOptions` | `Effect<string, PlatformError>` | Create a temporary file. The directory creation is functionally equivalent to `makeTempDirectory`. The file name will be a randomly generated string. |
2623
+ | **makeTempFileScoped** | `options?: MakeTempFileOptions` | `Effect<string, PlatformError, Scope>` | Create a temporary file inside a scope. Functionally equivalent to `makeTempFile`, but the file will be automatically deleted when the scope is closed. |
2624
+ | **open** | `path: string`, `options?: OpenFileOptions` | `Effect<File, PlatformError, Scope>` | Open a file at `path` with the specified `options`. The file handle will be automatically closed when the scope is closed. |
2625
+ | **readDirectory** | `path: string`, `options?: ReadDirectoryOptions` | `Effect<Array<string>, PlatformError>` | List the contents of a directory. You can recursively list the contents of nested directories by setting the `recursive` option. |
2626
+ | **readFile** | `path: string` | `Effect<Uint8Array, PlatformError>` | Read the contents of a file. |
2627
+ | **readFileString** | `path: string`, `encoding?: string` | `Effect<string, PlatformError>` | Read the contents of a file as a string. |
2628
+ | **readLink** | `path: string` | `Effect<string, PlatformError>` | Read the destination of a symbolic link. |
2629
+ | **realPath** | `path: string` | `Effect<string, PlatformError>` | Resolve a path to its canonicalized absolute pathname. |
2630
+ | **remove** | `path: string`, `options?: RemoveOptions` | `Effect<void, PlatformError>` | Remove a file or directory. By setting the `recursive` option to `true`, you can recursively remove nested directories. |
2631
+ | **rename** | `oldPath: string`, `newPath: string` | `Effect<void, PlatformError>` | Rename a file or directory. |
2632
+ | **sink** | `path: string`, `options?: SinkOptions` | `Sink<void, Uint8Array, never, PlatformError>` | Create a writable `Sink` for the specified `path`. |
2633
+ | **stat** | `path: string` | `Effect<File.Info, PlatformError>` | Get information about a file at `path`. |
2634
+ | **stream** | `path: string`, `options?: StreamOptions` | `Stream<Uint8Array, PlatformError>` | Create a readable `Stream` for the specified `path`. |
2635
+ | **symlink** | `fromPath: string`, `toPath: string` | `Effect<void, PlatformError>` | Create a symbolic link from `fromPath` to `toPath`. |
2636
+ | **truncate** | `path: string`, `length?: SizeInput` | `Effect<void, PlatformError>` | Truncate a file to a specified length. If the `length` is not specified, the file will be truncated to length `0`. |
2637
+ | **utimes** | `path: string`, `atime: Date \| number`, `mtime: Date \| number` | `Effect<void, PlatformError>` | Change the file system timestamps of the file at `path`. |
2638
+ | **watch** | `path: string` | `Stream<WatchEvent, PlatformError>` | Watch a directory or file for changes. |
2639
+
2640
+ Let's explore a simple example using `readFileString`:
2641
+
2642
+ ```ts
2643
+ import { FileSystem } from "@effect/platform"
2644
+ import { NodeFileSystem, NodeRuntime } from "@effect/platform-node"
2645
+ import { Effect } from "effect"
2646
+
2647
+ // const readFileString: Effect.Effect<void, PlatformError, FileSystem.FileSystem>
2648
+ const readFileString = Effect.gen(function* (_) {
2649
+ const fs = yield* _(FileSystem.FileSystem)
2650
+
2651
+ // Reading the content of the same file where this code is written
2652
+ const content = yield* _(fs.readFileString("./index.ts", "utf8"))
2653
+ console.log(content)
2654
+ })
2655
+
2656
+ NodeRuntime.runMain(readFileString.pipe(Effect.provide(NodeFileSystem.layer)))
2657
+ ```
2658
+
2659
+ # KeyValueStore
2660
+
2661
+ ## Overview
2662
+
2663
+ The `KeyValueStore` module provides a robust and effectful interface for managing key-value pairs. It supports asynchronous operations, ensuring data integrity and consistency, and includes built-in implementations for in-memory, file system-based, and schema-validated stores.
2664
+
2665
+ ## Basic Usage
2666
+
2667
+ The `KeyValueStore` interface includes the following operations:
2668
+
2669
+ - **get**: Retrieve a value by key.
2670
+ - **set**: Store a key-value pair.
2671
+ - **remove**: Delete a key-value pair.
2672
+ - **clear**: Remove all key-value pairs.
2673
+ - **size**: Get the number of stored pairs.
2674
+ - **modify**: Atomically modify a value.
2675
+ - **has**: Check if a key exists.
2676
+ - **isEmpty**: Check if the store is empty.
2677
+
2678
+ **Example**
2679
+
2680
+ ```ts
2681
+ import { KeyValueStore, layerMemory } from "@effect/platform/KeyValueStore"
2682
+ import { Effect } from "effect"
2683
+
2684
+ const program = Effect.gen(function* () {
2685
+ const store = yield* KeyValueStore
2686
+ console.log(yield* store.size) // Outputs: 0
2687
+
2688
+ yield* store.set("key", "value")
2689
+ console.log(yield* store.size) // Outputs: 1
2690
+
2691
+ const value = yield* store.get("key")
2692
+ console.log(value) // Outputs: { _id: 'Option', _tag: 'Some', value: 'value' }
2693
+
2694
+ yield* store.remove("key")
2695
+ console.log(yield* store.size) // Outputs: 0
2696
+ })
2697
+
2698
+ Effect.runPromise(program.pipe(Effect.provide(layerMemory)))
2699
+ ```
2700
+
2701
+ ## Built-in Implementations
2702
+
2703
+ The module provides several built-in implementations to suit different needs:
2704
+
2705
+ - **In-Memory Store**: `layerMemory` provides a simple, in-memory key-value store, ideal for lightweight or testing scenarios.
2706
+ - **File System Store**: `layerFileSystem` offers a file-based store for persistent storage needs.
2707
+ - **Schema Store**: `layerSchema` enables schema-based validation for stored values, ensuring data integrity and type safety.
2708
+
2709
+ ## Schema Store
2710
+
2711
+ The `SchemaStore` implementation allows you to validate and parse values according to a defined schema. This ensures that all data stored in the key-value store adheres to the specified structure, enhancing data integrity and type safety.
2712
+
2713
+ **Example**
2714
+
2715
+ ```ts
2716
+ import { KeyValueStore, layerMemory } from "@effect/platform/KeyValueStore"
2717
+ import { Schema } from "@effect/schema"
2718
+ import { Effect } from "effect"
2719
+
2720
+ // Define a schema for the values
2721
+ const Person = Schema.Struct({
2722
+ name: Schema.String,
2723
+ age: Schema.Number
2724
+ })
2725
+
2726
+ const program = Effect.gen(function* () {
2727
+ const store = (yield* KeyValueStore).forSchema(Person)
2728
+
2729
+ // Create a value that adheres to the schema
2730
+ const value = { name: "Alice", age: 30 }
2731
+ yield* store.set("user1", value)
2732
+ console.log(yield* store.size) // Outputs: 1
2733
+
2734
+ // Retrieve and validate the value
2735
+ const retrievedValue = yield* store.get("user1")
2736
+ console.log(retrievedValue) // Outputs: { _id: 'Option', _tag: 'Some', value: { name: 'Alice', age: 30 } }
2737
+ })
2738
+
2739
+ Effect.runPromise(program.pipe(Effect.provide(layerMemory)))
2740
+ ```
2741
+
2742
+ In this example:
2743
+
2744
+ - **Person**: Defines the structure for the values stored in the key-value store.
2745
+ - **store.set**: Stores a value adhering to `Person`.
2746
+ - **store.get**: Retrieves and validates the stored value against `Person`.