@@ -115,7 +115,7 @@ implement a minimal '`Hello World`' application. While it may be
115
115
tempting to skip ahead, a sound understanding of how to use Duct will
116
116
make later sections easier to follow.
117
117
118
- === Hello World
118
+ === First Steps
119
119
120
120
As mentioned previously, Duct uses a file, `duct.edn`, to define the
121
121
structure of your application. We'll begin by adding a new component
@@ -169,140 +169,173 @@ Hello World
169
169
170
170
Congratulations on your first Duct application!
171
171
172
- === Component Options
172
+ === The REPL
173
173
174
- One advantage of having a configuration file is that we can customize
175
- the behavior of components. Let's change our `hello` function to take
176
- a `:name` option.
174
+ Duct has two ways of running your application: `--main` and `--repl`.
177
175
178
- [,clojure]
179
- ----
180
- (ns tutorial.print)
176
+ In the previous section we started the application with `--main`, which
177
+ will *initiate* the system defined in the configuration file, and *halt*
178
+ the system when the process terminates.
179
+
180
+ The REPL is an interactive development environment.
181
181
182
- (defn hello [{:keys [name]}]
183
- (println "Hello" name))
182
+ [,shell]
183
+ ----
184
+ $ duct --repl
185
+ ✓ Loading REPL environment...
186
+ [Repl balance] Type :repl/help for online help info
187
+ user=>
184
188
----
185
189
186
- Now that `hello` expects an option, we'll need to add it to the
187
- `duct.edn` file .
190
+ In the REPL environment the system will not be initiated automatically.
191
+ Instead, we use the inbuilt `(go)` function .
188
192
189
193
[,clojure]
190
194
----
191
- {:system
192
- {:tutorial.print/hello {:name "World"}}}
195
+ user=> (go)
196
+ Hello World
197
+ :initiated
193
198
----
194
199
195
- Naturally this produces the same result as before when we run the
196
- application.
200
+ The REPL can be left running while source files updated. The `(reset)`
201
+ function will halt the running system, reload any modified source files,
202
+ then initiate the system again.
197
203
198
- [,shell ]
204
+ [,clojure ]
199
205
----
200
- $ duct --main
201
- ✓ Initiating system...
206
+ user=> (reset)
207
+ :reloading (tutorial.print)
202
208
Hello World
209
+ :resumed
203
210
----
204
211
205
- === Variables
206
-
207
- Sometimes we want to supply options from an external source, such as an
208
- environment variable or command line option. Duct allows variables, or
209
- *vars*, to be defined in the `duct.edn` configuration.
210
-
211
- Let's add a var to our configuration file.
212
+ The configuration defined by `duct.edn` can be accessed with `config`,
213
+ and the running system can be accessed with `system`.
212
214
213
215
[,clojure]
214
216
----
215
- {:vars
216
- {name {:arg name, :env NAME, :type :str, :default "World"
217
- :doc "The name of the person to greet"}}
218
- :system
219
- {:tutorial.print/hello {:name #ig/var name}}}
217
+ user=> config
218
+ #:tutorial.print{:hello {}}
219
+ user=> system
220
+ #:tutorial.print{:hello nil}
220
221
----
221
222
222
- This defines a var called `name` with two sources. In order of priority:
223
+ === Modules
223
224
224
- . A command-line argument `--name` (set via `:arg`)
225
- . An environment variable `$NAME` (set via `:env`)
225
+ A *module* groups multiple components together. Duct provides a number
226
+ of pre-written modules that implement common functionality. One of these
227
+ modules is `:duct.module/logging`.
226
228
227
- This value can be inserted into the system map with the `#ig/var` data
228
- reader. If the variable has no value, an error will be raised, so it's a
229
- good idea to set a default value using the `:default` key.
229
+ We'll first add the dependency to `deps.edn`.
230
230
231
- NOTE: The '`ig`' in `#ig/var` stands for _Integrant_. This is the
232
- library that Duct relies on to turn configurations into running
233
- applications.
231
+ [,clojure]
232
+ ----
233
+ {:deps {org.duct-framework/main {:mvn/version "0.1.0"}
234
+ org.duct-framework/module.logging {:mvn/version "0.6.4"}}
235
+ :aliases {:duct {:main-opts ["-m" "duct.main"]}}}
236
+ ----
237
+
238
+ Then we'll add the module to the `duct.edn` configuration.
234
239
235
- The `:type` of a var determines the data type it should be coerced into.
236
- Duct supports three types natively: `:str`, `:int` and `:float`. The
237
- default type when the key is omitted is `:str`.
240
+ [,clojure]
241
+ ----
242
+ {:system
243
+ {:duct.module/logging {}
244
+ :tutorial.print/hello {}}}
245
+ ----
238
246
239
- Duct integrates these vars into its help message. The `:doc` option
240
- specifies a description of the var.
247
+ Before the components are initiated, modules are *expanded*. We can see
248
+ what this expansion looks like by using the `--show` flag. This will
249
+ print out the expanded configuration instead of initiating it.
241
250
242
- [,shell,highlight=13 ]
251
+ [,shell]
243
252
----
244
- $ duct --help
245
- Usage:
246
- clojure -M:duct [--main | --repl]
247
- Options:
248
- -c, --cider Start an NREPL server with CIDER middleware
249
- --init Create a blank duct.edn config file
250
- -p, --profiles PROFILES A concatenated list of profile keys
251
- -n, --nrepl Start an NREPL server
252
- -m, --main Start the application
253
- -r, --repl Start a command-line REPL
254
- -s, --show Print out the expanded configuration and exit
255
- -v, --verbose Enable verbose logging
256
- -h, --help Print this help message and exit
257
- --name NAME The name of the person to greet
253
+ $ duct --main --show
254
+ {:duct.logger/simple {:appenders [{:type :stdout}]}
255
+ :tutorial.print/hello {}}
258
256
----
259
257
260
- The var can then be specified at the command line to produce different
261
- results.
258
+ The logging module has been replaced with the `:duct.logger/simple`
259
+ component.
260
+
261
+ The `--show` flag also works with the `--repl` command.
262
262
263
263
[,shell]
264
264
----
265
- $ duct --main --name=Clojurian
266
- ✓ Initiating system...
267
- Hello Clojurian
265
+ $ duct --repl --show
266
+ {:duct.logger/simple
267
+ {:appenders
268
+ [{:type :stdout, :brief? true, :levels #{:report}}
269
+ {:type :file, :path "logs/repl.log"}]}
270
+ :tutorial.print/hello {}}
271
+ ----
268
272
269
- $ NAME=Clojurist duct --main
270
- ✓ Initiating system...
271
- Hello Clojurist
273
+ But wait a moment, why is the expansion of the configuration different
274
+ depending on how we run Duct? This is because the `--main` flag has an
275
+ implicit `:main` profile, and the `--repl` flag has an implicit `:repl`
276
+ profile.
277
+
278
+ The `:duct.module/logging` module has different behaviors depending on
279
+ which profile is active. When run with the `:main` profile, the logs
280
+ print to STDOUT, but this would be inconveniently noisy when using a
281
+ REPL. So when the `:repl` profile is active, most of the logs are sent
282
+ to a file, `logs/repl.log`.
283
+
284
+ In order to use this module, we need to connect the logger to our
285
+ '`hello`' component. This is done via a *ref*.
286
+
287
+ [,clojure]
288
+ ----
289
+ {:system
290
+ {:duct.module/logging {}
291
+ :tutorial.print/hello {:logger #ig/ref :duct/logger}}}
272
292
----
273
293
274
- === References
294
+ The `#ig/ref` data reader is used to give the '`hello`' component access
295
+ to the logger. We use `:duct/logger` instead of `:duct.logger/simple`,
296
+ as keys have a logical hierarchy, and `:duct/logger` fulfils a role
297
+ similar to that of an interface or superclass.
275
298
276
- A Duct system can have multiple components, and components can
277
- communicate via references, which are divided into *refs* and
278
- *refsets*. A ref references exactly one other component; a refset
279
- references zero or more components.
299
+ NOTE: The '`ig`' in `#ig/var` stands for _Integrant_. This is the
300
+ library that Duct relies on to turn configurations into running
301
+ applications.
280
302
281
- To demonstrate how refs work, we'll divide our '`Hello World`'
282
- application into two functions .
303
+ Now that we've connected the components together in the configuration
304
+ file, it's time to replace the `println` function with the Duct logger .
283
305
284
306
[,clojure]
285
307
----
286
- (ns tutorial.print)
308
+ (ns tutorial.print
309
+ (:require [duct.logger :as log]))
310
+
311
+ (defn hello [{:keys [logger]}]
312
+ (log/report logger ::hello {:name "World"}))
313
+ ----
314
+
315
+ The `duct.logger/report` function is used to emit a log at the `:report`
316
+ level. This is a high-priority level that should be used sparingly, as
317
+ it also prints to STDOUT when using the REPL.
287
318
288
- (defn printer [{:keys [prefix]}]
289
- (partial println prefix))
319
+ You may have noticed that we've replaced the `"Hello World"` string with
320
+ a keyword and a map: `::name {:name "World"}`. This is because Duct is
321
+ opinionated about logs being data, rather than human-readable strings. A
322
+ Duct log message consists of an *event*, a qualified keyword, and a map
323
+ of *event data*, which provides additional information.
290
324
291
- (defn hello [{:keys [name output]}]
292
- (output "Hello" name))
325
+ When we run the application, we can see what this produces.
326
+
327
+ [,shell]
328
+ ----
329
+ $ duct --main
330
+ ✓ Initiating system...
331
+ 2024-11-23T18:59:14.080Z :report :tutorial.print/hello {:name "World"}
293
332
----
294
333
295
- The `printer` component returns a function that prints its arguments
296
- with a custom prefix. The `hello` component takes a `:output` option
297
- that it uses to output a message. In order to connect these two
298
- components in the `duct.edn` file, we use the `#ig/ref` data reader.
334
+ When using the REPL, we get a more concise message.
299
335
300
- [,clojure ]
336
+ [,shell ]
301
337
----
302
- {:system
303
- {:tutorial.print/printer
304
- {:prefix ">>"}
305
- :tutorial.print/hello
306
- {:name "World"
307
- :output #ig/ref :tutorial.print/printer}}}
338
+ user=> (go)
339
+ :initiated
340
+ :tutorial.print/hello {:name "World"}
308
341
----
0 commit comments