From 966d2ca8bd2baa4b8ef61de7963d82c90296ffa6 Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Wed, 3 Jun 2026 23:29:53 +0400 Subject: [PATCH 01/11] Restructure tutorial; add Why DI? and Reference scaffolding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rewrite tutorial chapters 1–11 with hooks, structured sections, and forward-references - Add Why DI? as a top-level intro page - Add How-to with Tips and Multi-arity services (moved from Tutorial) - Add Reference section with a Middleware types stub - Reorder cljdoc.edn TOC to match the new structure - Update tutorial-to-md.sh to generate from how_to/ and reference/ - Update CI to commit the new doc subdirectories - Add planning artifacts under doc/_journey.md and doc/_structure.md Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 2 +- .gitignore | 3 + doc/_journey.md | 1135 +++++++++++++++++ doc/_structure.md | 264 ++++ doc/cljdoc.edn | 30 +- doc/why_di.md | 139 ++ script/tutorial-to-md.sh | 64 +- .../multi_arity_service_test.clj} | 15 +- test/darkleaf/di/how_to/tips_test.clj | 43 + .../di/reference/middleware_types_test.clj | 11 + test/darkleaf/di/tutorial/a_intro_test.clj | 83 +- .../di/tutorial/b_dependencies_test.clj | 67 +- test/darkleaf/di/tutorial/c_stop_test.clj | 42 +- .../d_interactive_development_test.clj | 55 + .../di/tutorial/l_registries_test.clj | 51 +- .../di/tutorial/m_abstractions_test.clj | 84 +- test/darkleaf/di/tutorial/n_env_test.clj | 56 +- test/darkleaf/di/tutorial/o_data_dsl_test.clj | 74 +- test/darkleaf/di/tutorial/p_derive_test.clj | 46 +- .../di/tutorial/q_starting_many_keys_test.clj | 30 +- .../di/tutorial/x_update_key_test.clj | 51 +- 21 files changed, 2135 insertions(+), 210 deletions(-) create mode 100644 doc/_journey.md create mode 100644 doc/_structure.md create mode 100644 doc/why_di.md rename test/darkleaf/di/{tutorial/y_multi_arity_service_test.clj => how_to/multi_arity_service_test.clj} (51%) create mode 100644 test/darkleaf/di/how_to/tips_test.clj create mode 100644 test/darkleaf/di/reference/middleware_types_test.clj create mode 100644 test/darkleaf/di/tutorial/d_interactive_development_test.clj diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2e28943..8e21c291 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,7 @@ jobs: GIT_COMMITTER_NAME: ${{ github.actor }} GIT_COMMITTER_EMAIL: ${{ github.actor }}@users.noreply.github.com run: | - git add -f doc/tutorial + git add -f doc/tutorial doc/how_to doc/reference git add Readme.md git commit -m "Generate cljdoc docs for ${GITHUB_REF_NAME}" diff --git a/.gitignore b/.gitignore index 6476b4cc..9a5164b0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,8 @@ /target /pom.xml /doc/tutorial/ +/doc/how_to/ +/doc/reference/ +#/doc/_*.md .clj-kondo/ .lsp/ \ No newline at end of file diff --git a/doc/_journey.md b/doc/_journey.md new file mode 100644 index 00000000..4bf1efde --- /dev/null +++ b/doc/_journey.md @@ -0,0 +1,1135 @@ +# Карта читателя по туториалу — черновик + +Рабочий артефакт Этапа 0. Не для публикации. +Формат для каждой главы: вход → выход → комментарий. + +Метки: 🟢 нормально / 🟡 есть проблемы / 🔴 нужна серьёзная переработка. + +Оставляй реакции под каждым пунктом строкой `> автор:`, либо +правь/вычёркивай сам текст, если что-то описано неточно. Когда +закончишь — переходим к Этапу 1 (новая структура + `cljdoc.edn`). + +--- + +## Base (текущий порядок) + +### 1. Intro — `a_intro_test.clj` + +- **Вход:** базовый Clojure. +- **Выход:** умеет вызвать `di/start`; знает `root`, `di/stop`, + `with-open`; отличает `:component` от service; видел зависимость- + плейсхолдер; видел interactive redef. +- 🔴 **Проблема:** глава учит 5+ концептам сразу. Hook отсутствует + («Let's start. In this chapter I'll show you how to deal with + components» — это не hook). Interactive redef — отдельный концепт, + не должен ехать в Intro. + +> автор: +одна из проблем - мой тимлид говорит, что a, b, c, d это абстрактные шутки и ему не удобно. +но я инженер-математик по образованию и мне ок, я так показываю суть отношений. + +почему там много разных концепций, тут все базовые концепции. +и если выделять по одной, то имхо будет мало текста плюс много воды + +### 2. Dependencies — `b_dependencies_test.clj` + +- **Вход:** знает, что такое component. +- **Выход:** пишет зависимости через assoc destructuring; понимает + `:or`, `:as deps`; видел ошибку missing dep; **уже использует + registry** для подмены deps. +- 🟡 **Проблема:** глава молча использует `di/start` с map-аргументом + (registry), но термин «registry» не вводится. Это путает, когда + читатель доходит до главы 4 (Registries). + +> автор: +то, что не вводится реестр, это осознанно, иначе как этот граф разорвать? +я решил, что лучше потом объяснить + +### 3. Stop — `c_stop_test.clj` + +- **Вход:** знает про components. +- **Выход:** умеет навесить `::di/stop`; знает `memfn`. +- 🟡 **Проблема:** очень короткая, нет «когда тебе это понадобится». + Stop логически должен идти сразу после Intro, но Intro уже использует + `with-open`. Возможно, слить start/stop lifecycle в одну главу. + +> автор: +про логичность - я хз. вопрос опять про очередность. и как граф сделать линейным повествованием. +если бы я сделал иначе тогда, сейчас ты бы снова сказал, а почему у тебя тут не stop, а зависимости. + +### 4. Registries — `l_registries_test.clj` + +- **Вход:** уже видел, как map передаётся в `di/start`. +- **Выход:** знает термин «registry»; знает, что регистров может быть + несколько и работает «last wins»; знает про seqable. +- 🟡 **Проблема:** наполовину дублирует главу 2. Сама концепция + registry должна вводиться *в* главе 2. + +> автор: + +### 5. Abstractions — `m_abstractions_test.clj` + +- **Вход:** знает про symbol-ключи. +- **Выход:** использует keyword-ключи для отвязки от vars. +- 🔴 **Проблема:** это **главная фича DI** (отвязка от конкретных + vars → подменяемые реализации), но глава самая короткая и + hand-wavy. «Later in the main function you will be able to bind + all parts» — не объясняет, почему это важно. Нет руководства + «symbol vs keyword: когда что брать». + +> автор: +я бы не назвал ее главной фичей. главное это про update-key. + +когда какое брать - это есть в docstring. +но соглашусь. + +### 6. Env — `n_env_test.clj` + +- **Вход:** знает разные виды ключей. +- **Выход:** читает env-переменные через string-ключи; знает + `di/env-parsing` middleware; знает синтаксис `:env.long/X`. +- 🟢 Содержательно нормально. Hook можно усилить + («подключаем систему к окружению»). + +> автор: +видимо тут перевод кривой, я не понимаю смысл «подключаем систему к окружению» + +### 7. Data DSL — `o_data_dsl_test.clj` + +- **Вход:** знает symbol / keyword / string ключи. +- **Выход:** пишет `di/template`, `di/ref`, `di/opt-ref`. +- 🟡 **Проблема:** 27 строк, hook есть («data-DSLs like reitit»), + но пример абстрактный — читатель не видит реалистичную reitit- + конфигурацию. Концепт мощный, недопродан. + +> автор: +можешь посмотреть в gmonit/collector, в проетах лежит + +### 8. Derive — `p_derive_test.clj` + +- **Вход:** знает env-ключи и templates. +- **Выход:** применяет функцию к собранному значению. +- 🟡 **Проблема:** мотивация «components may have a complex + structure» расплывчатая. Реалистичный пример (parse env var) + есть, но он крошечный. + +> автор: + +это было в эпоху до LLM, и мне сложно писать текст + +### 9. Starting many keys — `q_starting_many_keys_test.clj` + +- **Вход:** знает `di/start` и `with-open`. +- **Выход:** стартует вектор/map ключей; использует `di/with-open` + с destructuring. +- 🟢 Технически нормально, но это скорее **how-to/recipe**, + а не шаг прогрессирующего туториала. + +> автор: + +это важная часть при написании тестов. +и важно, если нужно стартануть не только веб-сервер + +### 10. Multimethods — `r_multimethods_test.clj` + +- **Вход:** знает services и deps. +- **Выход:** навешивает `::di/deps` на defmulti. +- 🟡 Узкая ниша. Кандидат на переезд в How-to. + +> автор: +в gmonit это применяется, но узкая ниша - да. + +--- + +## Advanced (текущий порядок) + +### 11. Add a side dependency — `x_add_side_dependency_test.clj` + +- **Вход:** знает, что бывают middlewares. +- **Выход:** подключает миграции / init. +- 🟢 Чёткий use-case. **Это recipe.** + +> автор: +с этой фичей бывают проблемы, но можно наверное не распространяться, вроде все уладили + +### 12. Update key — `x_update_key_test.clj` + +- **Вход:** знает components. +- **Выход:** понимает decorator pattern и cross-module composition. +- 🟢 **Хорошо написано** (свежий rewrite). Это эталон voice. + Но концепт настолько фундаментальный для DI, что должен быть + в Base, а не в Advanced. + +> автор: +хз, про хорошо написан, это LLM сгенеренный +концепт важный, но это тема со звездочкой так-то и я не хотел давать его сразу + +### 13. Log — `x_log_test.clj` + +- 🟢 Хорошо написано (свежее). Это recipe. + +> автор: + +### 14. Inspect — `x_inspect_test.clj` + +- 🔴 **Это reference, а не tutorial.** 377 строк, перечислены все + формы `:description`. Полезно, но не как глава туториала. Должно + стать reference-страницей со ссылкой из туториала. + +> автор: +наверное да + +### 15. Graceful stop — `y_graceful_stop_test.clj` + +- 🟡 Описание поведения (что происходит при сбое), а не how-to. + Ближе к explanation/reference. + +> автор: + +### 16. Multi arity service — `y_multi_arity_service_test.clj` + +- 🟡 Узкая ниша. Recipe. + +> автор: +это пиздец какая широкая ниша + +### 17. Multi system — `z_multi_system_test.clj` + +- 🟢 Чёткий recipe. + +> автор: + +### 18. Two databases — `z_two_databases_test.clj` + +- 🟢 Чёткий recipe + введение в кастомный `Factory`. Но `Factory` + нигде раньше не объяснён. + +> автор: + +--- + +## Cross-cutting наблюдения + +### O1. Нет главы «Why DI? What problem?» + +Читатель попадает сразу на `di/start` без мотивации. У Integrant +в Readme такая секция есть. + +> автор: +может быть зачаток есть в readme. мне было тяжело это написать, но давай напишем. + +### O2. Tutorial и How-to не разделены + +6 из 8 глав «Advanced» — это recipes («как сделать X»), а не +следующие шаги обучения. Это и есть источник ощущения «сухости». + +> автор: + +### O3. Недавно переписанные главы — другой voice + +Update key, Log, Inspect, Ns publics — у них есть hook, +объяснение «зачем», cross-ссылки. Остальные суше. +**Это и есть target voice.** + +> автор: +это сгенеренное, а "сухое" писал я руками + +### O4. Registry вводится дважды + +Неявно в главе 2, явно в главе 4. Слить. + +> автор: + +### O5. Abstractions (главная фича) — самая короткая глава + +Несоответствие важность/объём. + +> автор: + +### O6. Update key должен быть в Base + +Это не «продвинутый трюк», это способ собирать модули. Без него +непонятно, как DI решает задачу композиции. + +> автор: + +### O7. `Inspect`-как-глава-туториала не работает + +Это reference-материал. Нужна другая роль. + +> автор: + +### O8. Ns publics не в TOC + +`x_ns_publics_test.clj` есть в файлах, но не в `cljdoc.edn`. +Намеренно? Сам автор главы пишет в конце «`->memoize` is the +preferred way now» — то есть описывает legacy. Или выкинуть, +или явно отметить как историческое примечание. + +> автор: +ну пусть будет, зачем выкидывать, я не пометил его как deprecated + +--- + +## Что нужно от тебя + +Пройдись по каждой главе и каждому O-пункту. Под строкой `> автор:` +напиши свою реакцию или поправь/вычеркни текст, если описано +неточно. Когда закончишь — скажи «готово», я прочитаю файл и +переходим к Этапу 1. + +Если по ходу появятся темы, которые я не учёл (например, важные +use-cases из issues/вопросов пользователей) — добавляй в конец +файла свободным текстом. + +ниже переписка из реддита + + +1) + +Аватар u/serefayar +serefayar +• +3 г. назад +Nice work! + +I agree with u/telenieko, the page definitely needs such a section. + + + +Нравится +3 + +Не нравится + +Ответить + +Награда + +Поделиться + +Аватар u/kuzmin_m +kuzmin_m +Автор +• +3 г. назад +I don't want to compare different solutions, that wouldn't be polite to them. But I have written a draft of Motivation section. What do you think? + +Motivation + +I think a function that uses a stateful component should also be a component. It's ok to have 97 functions and 3 stateful components. So creating of stateless components should be as cheap as possible. I don't want to build the system out of these 3 stateful components and manually pass the system to each function. And I don't want to define my 97 functions as components. + +I also need something like Clojure namespaces. I want to define dependencies of a component with that component, just like I do it with Clojure namespaces. I don't want to define the dependency graph in a single file. I want to define regular functions. + +I want to build applications separated on low coupled engines or subsystems. So I need a solution to define abstractions and extend (modify) existing common components. For example, in my application, each subsystem has its own route data and the single reitit handler. See di/update-key. + +In Clojure it is common to use data DSL. I use reitit, and DI allows you to inject components into plain Clojure data. See di/template. + +For some functions I want to add instrumentation. For example, I wrap some functions with NewRelic instrumentation without modifying the source code of those functions. Maybe I'll add schema/spec for my functional components with di/instrument. + +I want to have a framework that is smart enough to stop already started components if the system fails to start. + +Finally, I want to be able to dynamically redefine my 97 functions during development without having to restart the whole system. + + + +Нравится +4 + +Не нравится + +Ответить + +Награда + +Поделиться + +Аватар u/weavejester +weavejester +• +3 г. назад +I don't want to compare different solutions, that wouldn't be polite to them. + +I don't consider this to be rude, personally. If you want to compare DI to Integrant, please do so. It's rare that a single solution is good in all circumstances, and it's often the case that later solutions are better than previous ones. + + + +Нравится +7 + +Не нравится + +Ответить + +Награда + +Поделиться + +Аватар u/kuzmin_m +kuzmin_m +Автор +• +3 г. назад +https://darkleaf.github.io/di/notebooks/integrant.html + +CC u/roguas, u/coffeesounds, u/rmuslimov, u/serefayar + + + +Нравится +2 + +Не нравится + +Ответить + +Награда + +Поделиться + +Аватар u/weavejester +weavejester +• +3 г. назад +Thanks for writing this up! + +A small correction regarding the Error Handling section: when Integrant throws an error, it includes the partially started system in the exception, to allow you to halt it if you choose. If you're using the Integrant-REPL library, then the init function does this automatically. + +In the Handlers section, I think you may have misunderstood what suspending and resuming is for. If you eval a DI component function again, it presumably doesn't affect an already running system, right? + + + +Нравится +1 + +Не нравится + +Ответить + +Награда + +Поделиться + +Аватар u/kuzmin_m +kuzmin_m +Автор +• +3 г. назад +• +, Отредактированные 3 г. назад +the init function does this automatically. + +Thanks, I didn't know that. + +It only catches clojure.lang.ExceptionInfo. Any component, such as database connector, will in most cases throw any Throwable. + +It loses exceptions. DI uses suppressed exception, so user will see original exception and all possible exceptions thrown on stop. + +test + +combine-throwable + +stop + +it presumably doesn't affect an already running system, right? + +Right. UPD. It is right for stateful components. If it is a service (stateless component) the behavior of running system will change. See an example + +I think you may have misunderstood what suspending and resuming is for. + +I think they are useful when someone redefines defmethod of stateless components. Maybe I'm wrong. + + + +Нравится +1 + +Не нравится + +Ответить + +Награда + +Поделиться + +Аватар u/weavejester +weavejester +• +3 г. назад +It only catches clojure.lang.ExceptionInfo. Any component, such as database connector, will in most cases throw any Throwable. + +Yes, because any exceptions arising from a build are wrapped in an ExceptionInfo in order to carry across additional context. + +If you're using Integrant REPL, any Throwable exception will trigger a halt of the partially initiated system, before being rethrown for you to inspect along with additional context. + +It loses exceptions. + +I'm not sure I understand. Do you mean that it stops at the first exception? + +I think they are useful when someone redefines defmethod of stateless components. Maybe I'm wrong. + +No, their primary use is maintaining resources across restarts. For example, suppose you wanted to restart and rebuild your system, but you also didn't want to close any connections your system might have open. Suspend/resume allows you to persist open resources across restarts (where possible to do so). + + + +Нравится +1 + +Не нравится + +Ответить + +Награда + +Поделиться + +Аватар u/kuzmin_m +kuzmin_m +Автор +• +3 г. назад +Yes, because any exceptions arising from a build are wrapped in an ExceptionInfo in order to carry across additional context. + +Exactly. Thank you for the explanation. + +I'm not sure I understand. Do you mean that it stops at the first exception? + +Let's look at a trivial system: a list. A(root) depends on B, B depends on C. + +If a BError occurs when starting B, C will already be running. DI will attempt to stop C. If the attempt fails with CError, the user will get the original BError with the CError suppressed. + +JVM has a feature called suppressed Exceptions. (.addSuppressed b-error c-error). (.getSuppressed b-error) will return [c-error]. + +Some IDE prints suppressed errors. I have heard that IDEA does this. + +No, their primary use is maintaining resources across restarts. + +Ok. I've never been faced with this kind of task before. + + + +Нравится +1 + +Не нравится + +Ответить + +Награда + +Поделиться + +Аватар u/weavejester +weavejester +• +3 г. назад +Let's look at a trivial system: a list. A(root) depends on B, B depends on C. + +If a BError occurs when starting B, C will already be running. DI will attempt to stop C. If the attempt fails with CError, the user will get the original BError with the CError suppressed. + +In this case, Integrant REPL wraps CError in an ExceptionInfo, which has a key :init-exception which references the original wrapped BError. All the exceptions are captured; none are lost. + +JVM has a feature called suppressed Exceptions. + +That's interesting; I wasn't aware of that feature. It might be an idea to use that in addition to (or instead of) the :init-exception key. + + +Нравится +2 + +Не нравится + +Ответить + +Награда + +Поделиться + + +Еще 2 ответа +roguas +• +3 г. назад +Ok, lets talk. Why not integrant? + + + +Нравится +7 + +Не нравится + +Ответить + +Награда + +Поделиться + +[удалено] +• +3 г. назад +If it's one type of framework Clojure has in spades, its component frameworks. + + +Нравится +4 + +Не нравится + +Ответить + +Награда + +Поделиться + +Аватар u/kuzmin_m +kuzmin_m +Автор +• +3 г. назад +Have you seen the [tutorial](https://darkleaf.github.io/di/#tutorial)? + + +Нравится +1 + +Не нравится + +Ответить + +Награда + +Поделиться + +telenieko +• +3 г. назад +Maybe the page could have a section comparing this to other solutions and what motivated you to create a new one + + +Нравится +6 + +Не нравится + +Ответить + +Награда + +Поделиться + +rebcabin-r +• +3 г. назад +Excuse my ignorance: Are things like this DI, Integrant, Component for mocking / testing web services? From the docs, I can't see really what use-cases these things are for. (I'm not a web developer, doing compilers and embedded systems mostly) + + + +Нравится +6 + +Не нравится + +Ответить + +Награда + +Поделиться + +Аватар u/weavejester +weavejester +• +3 г. назад +Some applications are systems made from a number of smaller, interdependent components, and often these components require some form of setup and shutdown code. + +For example, a web service might consist of a web server, a database connection pool and a queued pool of workers. The web server would have routes, some of which might require a database connection, some of which might require the worker queue. + +It's perfectly possible to write code to manually manage starting up and shutting down each component in the right order, and to manually pass each component to the right function. However, a dependency injection framework automates some of the work; you define the dependency tree declaratively, and the framework handles the rest. + +The advantage (aside from writing a little less code) is that the dependency tree for the application, and often any associated configuration, is defined in one location. This is good for readability, but also allows components to be more easily swapped out for mocks, or to alter their configuration. + + + +Нравится +5 + +Не нравится + +Ответить + +Награда + +Поделиться + +rebcabin-r +• +3 г. назад +I see. Thank you for your kind reply! In other settings, e.g., pytest, I'll use "fixtures" for such use-cases, including the control of order-of-activation. This sounds like an interesting new approach for me to learn about. Perhaps I've been doing more than I need to do or suffering other pessimalities :) + + +Нравится +1 + +Не нравится + +Ответить + +Награда + +Поделиться + +coffeesounds +• +3 г. назад +What’s wrong with Component? + + +Нравится +4 + +Не нравится + +Ответить + +Награда + +Поделиться + +rmuslimov +• +3 г. назад +Integrant. + + +Нравится +2 + +Не нравится + +Ответить + +Награда + +Поделиться + +slifin +• +3 г. назад +Does anyone remember what that component library was that used Pathom 3 as part of its implementation? + + + +Нравится +1 + +Не нравится + +Ответить + +Награда + +Поделиться + +slifin +• +3 г. назад +Ah here it is + +https://www.reddit.com/r/Clojure/comments/xmu1ah/nivekuilnexus_ergonomic_dependency_injection_via/ + + +Нравится +1 + +Не нравится + +Ответить + +Награда + +Поделиться + +Раздел «Информация о сообществе» +r/Clojure +В сообществе +Clojure +Clojure is a dynamic, general-purpose programming language, combining the approachability and interactive development of a scripting language with an efficient and robust infrastructure for multithreaded programming. + +Показать больше +Создано 23 июл. 2008 г. +Публичное +5,4 тыс. +посетителя в неделю +63 +опубликованных материала за неделю +достижения сообщества +Старейшина +Старейшина +1 разблокировано + +Просмотреть все +Закладки сообщества +Clojure Reddit Chat +Правила r/Clojure +1 +Please stay on topic. This is the Clojure / ClojureScript subreddit. +2 +Keep job posts of any kind to replies to the recurring monthly "Who's hiring" post. +Resources +Finding information about Clojure + +Clojure Homepage +A Clojure Newbie Guide +Clojure Documentation +Clojure Cheat Sheet +ClojureScript Cheat Sheet +Clojure by Example +A History of Clojure +API Reference + +ClojureDocs API reference +CljDoc +Clojure Guides + +Getting Started +Clojure Distilled Beginner Guide +Clojure Style Guide +Clojure for the Brave and True +Clojure from the ground up +ClojureScript in 15 minutes +Practice Problems + +Wonderland Katas +Clojure Koans +Interactive Problems + +Maria Cloud +4Clojure +ClojureScript Koans +codewars +Clojure Videos + +Clojure TV +Clojure Content on InfoQ +Full Disclojure +The Clojure Language +Misc Resources + +StackOverflow info page +Clojure Events +The Clojure Community + +#clojure on Libera.Chat +Clojure user groups +ClojureScript user groups +Clojure Q&A +Clojure Slack Channel +Clojurians-Zulipchat +Discljord Discord +Clojurians Discord UNMODERATED +Clojureverse: a forum for and by the Clojure community +matrix/riot-im Clojure room +Clojure Books + +The Joy of Clojure +Clojure Programming +Clojure In Action +Programming Clojure +Web Development with Clojure +Clojure Cookbook +Professional Clojure +Living Clojure +Getting Clojure +Tools & Libraries + +Leiningen - Package management +Deps and CLI Guide - Dependency management and CLI +nREPL - Networked REPL +Gorilla REPL - A rich REPL for Clojure in the notebook style +Clojars - Clojure library repository +The Clojure Toolbox - a list of popular Clojure libraries +Clojure Editors + +Calva Visual Studio Code +Emacs CIDER +clojure-mode.el - Emacs mode +Emacs Prelude - a gentler Emacs mode +vim-fireplace - Vim! +Conjure - Conjure for NeoVim +Light Table - interactive Clojure IDE +Cursive - Clojure support for IntelliJ +Counterclockwise - Clojure support for Eclipse +Nightcode - a beginner friendly Clojure editor +Sublime Text 4 +Atom Info +clojureVSCode - Clojure support for Visual Studio Code. +Web Platforms + +Biff +re-frame +Hoplon +kit +Luminus +Clojure Jobs + +Brave Clojure Jobs +Functional Jobs +Functional Works +Clojure Events +Clojure real-world-data 61 +June 5, 2026 • 18:00 +online On Friday, we’ll have the usual weekly meeting of the real-world-data group. As usual, we will focus on community projects, library development, and documentation, but people are invited to propose additional topics to discuss and share. The main topics for the coming meetings will be documentation and additional updates. Updates about the agenda will be shared in the group chat: the real-world-data channel at the Zulip chat (requires login). If you wish to join the group, please reach out beforehand, and we’ll add you to the calendar event. parens1920×1440 98.2 KB Zulip: https://clojurians.zulipchat.com/#narrow/stream/262224-events/near/599011186 + + + +2) + +Аватар u/vvwccgz4lh +vvwccgz4lh +• +4 г. назад +What about shutting it down? +Edit: https://github.com/darkleaf/di/blob/master/example/src/example/core.clj#L50 + + + +Нравится +4 + +Не нравится + +Ответить + +Награда + +Поделиться + +Аватар u/kuzmin_m +kuzmin_m +Автор +• +4 г. назад +In example app: 1, 2 + +In tests: 3 + + +Нравится +2 + +Не нравится + +Ответить + +Награда + +Поделиться + +Аватар u/dustingetz +dustingetz +• +4 г. назад +Love your projects, thinking about all the right things + + + +Нравится +4 + +Не нравится + +Ответить + +Награда + +Поделиться + +Аватар u/kuzmin_m +kuzmin_m +Автор +• +4 г. назад +Thanks! + + + +Нравится +1 + +Не нравится + +Ответить + +Награда + +Поделиться + +exclaim_bot +• +4 г. назад +Thanks! + +You're welcome! + + +Нравится +-1 + +Не нравится + +Ответить + +Награда + +Поделиться + +Аватар u/agumonkey +agumonkey +• +4 г. назад +isn't DI a hybrid kind of variable / environment passing ? + + + +Нравится +1 + +Не нравится + +Ответить + +Награда + +Поделиться + +Аватар u/kuzmin_m +kuzmin_m +Автор +• +4 г. назад +Could you provide some examples? I don't understand the question. + + + +Нравится +1 + +Не нравится + +Ответить + +Награда + +Поделиться + +Аватар u/agumonkey +agumonkey +• +4 г. назад +Well, I'm not sure I ever understood DI fully, so it's a noob question if you will. But it seems very close to dynamic variables. + +(defn f [x] + (+ *dyn-var* x)) +dyn-var being redefineable from outer context + + + +Нравится +1 + +Не нравится + +Ответить + +Награда + +Поделиться + +Аватар u/kuzmin_m +kuzmin_m +Автор +• +4 г. назад +Ah, there are common things. + +The differences are: + +DI explicitly defines dependencies + +DI starts/stops stateful components + +I've wrote the tutorial. I hope it helps you understand concept. + + + +Нравится +5 + +Не нравится + +Ответить + +Награда + +Поделиться + +Аватар u/agumonkey +agumonkey +• +4 г. назад +dank u + + +Нравится +2 + +Не нравится + +Ответить + +Награда + +Поделиться + +Аватар u/didibus +didibus +• +4 г. назад +DI just means that you pass in dependencies instead of using globals directly. + +Dynamic vars would be one way to do DI, as you can redefine dependencies by binding them. + +Passing the dependencies as functions arguments or inside a context map is another way. + +Edit: referring to the concept of dependency injection,not this library specifically. + + + +Нравится +2 + +Не нравится + +Ответить + +Награда + +Поделиться + +Аватар u/agumonkey +agumonkey +• +4 г. назад +yeah, thanks + + +Нравится +1 + +Не нравится + +Ответить + +Награда + +Поделиться diff --git a/doc/_structure.md b/doc/_structure.md new file mode 100644 index 00000000..b61266f9 --- /dev/null +++ b/doc/_structure.md @@ -0,0 +1,264 @@ +# Новая структура туториала — v6 + +Артефакт Этапа 1. Не для публикации. +v6 — точечные правки в процессе Этапа 2. + +Изменения v5 → v6: + +- **Starting many keys** возвращён в Tutorial (после Environment + variables, перед Wiring inside data). Причина: Wiring inside + data использует мульти-ключ для теста, без введения это + спотыкает читателя. + +Изменения v4 → v5: + +- **Multi-arity services** возвращён в How-to (как было предложено + изначально). + +--- + +## Что я нашёл по твоим указаниям из v2 + +**Docstrings в `darkleaf.di.core`.** Я их прочитал. Несколько +наблюдений для будущей работы: + +- Docstring у `start` уже содержит 9-строчный код-пример со + стеком middlewares — хороший материал для главы 5 (Registries) + или 10 (Composition). +- Docstring у `Factory` (`protocols.clj`) очень крепкий, его можно + взять почти буквально для Reference-страницы. +- `update-key`, `env-parsing`, `ns-publics`, `log`, `add-side-dependency`, + `inspect` — все documented. В Этапе 2 не будем выдумывать + формулировки с нуля, а будем опираться на эти docstrings. + +**Тесты для Factory.** `dependency_types_test.clj` — идеальный +короткий пример кастомной фабрики (factory с required vs optional, +detection циклов). Эту мини-фабрику использую как канонический +пример в Reference-странице Factory. + +**Registry как middleware на registries.** Подтверждаю твоё +наблюдение. В `start` registry — это функция `key -> Factory`, +а map это её частный случай. Это меняет структуру (см. ниже). + +--- + +## Принципы (зафиксировано) + +1. **Tutorial и How-to разделены.** Tutorial линейно; How-to — + независимые рецепты под задачу. +2. **Главная идея библиотеки — композиция через `update-key`.** +3. **Stop — упоминается рано, продолжается в конце.** +4. **Interactive redef — отдельная глава туториала.** +5. **Reddit-Motivation — основа «Why DI?» (перепишу своими словами, без цитат).** +6. **«Modules» — не наш термин.** Глава 10 называется + «Composition with `update-key`». +7. **Реальный пример (gmonit) — деобфусцированные точечные вставки, + не сквозной пример.** +8. **Граф концептов не разрываем:** registry упоминается рано, + формально вводится в гл.5, как middleware-chain раскрывается + в гл.10. + +### Про math-style имена (`a`, `b`, `c`) + +Ты спросил моё мнение. Вот оно: + +В сообществе Clojure документация в основном использует +реалистичные имена (`:database`, `:web-server`, `:scheduler`) — +это даёт читателю якорь «это всё для настоящих приложений». +С другой стороны, в туториале, где фокус на **механике связывания**, +абстрактные имена убирают шум: читатель не отвлекается на то, +что такое scheduler, и смотрит как именно `a` зависит от `b`. + +Сочетание, которое мы уже зафиксировали — реалистичные имена +в **главе 1 «Your first system»** (чтобы тимлид не споткнулся +и читатель видел «это для настоящих апп»), и `a`/`b`/`c` +в остальных главах — стандартный приём в учебных текстах +(«concretize then abstract»). Это нормально и осознанно. + +Ничего лишнего изобретать не нужно. Твои буквы остаются. + +> автор: + +--- + +## Верхнеуровневое дерево + +``` +- Why DI? ← новая глава +- Tutorial ← линейно, 12 глав +- How-to ← независимые рецепты +- Reference ← Factory, Inspect, Middleware types +- Example app ← без изменений +- Integrant vs DI ← без изменений +``` + +> автор: + +--- + +## Tutorial — 12 глав + +| # | Глава | Источник | +|----|-----------------------------|--------------------------------------| +| 1 | Your first system | `a_intro` (без interactive redef) | +| 2 | Dependencies | `b_dependencies` | +| 3 | Stopping components | `c_stop` | +| 4 | Interactive development | секция из `a_intro` + новый материал | +| 5 | Registries | `l_registries` | +| 6 | Abstractions | `m_abstractions` (расширяем) | +| 7 | Environment variables | `n_env` | +| 8 | Starting many keys | `q_starting_many_keys` | +| 9 | Wiring inside data | `o_data_dsl` (gmonit-пример) | +| 10 | Transforming values | `p_derive` | +| 11 | Composition with `update-key`| `x_update_key` (повышаем) | +| 12 | Graceful failures | `y_graceful_stop` | + + +### Логика прогрессии + +- **1–2:** запустили систему, объявили зависимости. +- **3:** stop. По умолчанию ничего не нужно; `::di/stop` цепляется + метаданными только когда он нужен. К концу гл.3 у читателя — + полная картина статической системы. +- **4:** runtime story — REPL workflow, redef сервисов. + «Ты не обязан перезапускать систему, чтобы итерироваться». + Stuart Sierra "Reloaded Workflow" — ссылка. +- **5:** регистры. Представляем как map ключей в значения и стек + map'ов с last-wins. Forward-link на Reference (Middleware + types) — там общая форма «registry = функция key→Factory» и + список всех middlewares. +- **6–7:** отвязка от vars (keyword) и от окружения (string). +- **8:** запуск нескольких ключей за раз — Indexed/ILookup root, + `di/with-open`. Нужно до Wiring inside data, чтобы тест ref+template + можно было записать через сравнение equality. +- **9–10:** работа с данными — встраиваем deps в data-DSL, + трансформируем значения. +- **11:** кульминация. `update-key` как главный инструмент + композиции: декорирование built value + расширение shared + коллекции через модули. Это то, ради чего был весь путь. +- **12:** что происходит при сбое старта. + +--- + +## How-to — 8 рецептов + +Порядок неважен; читаются независимо. + +| Рецепт | Источник | +|---------------------------------|------------------------------------------| +| Side dependencies (миграции) | `x_add_side_dependency` | +| Multimethod services | `r_multimethods` | +| Multi-arity services | `y_multi_arity_service` | +| Multiple systems | `z_multi_system` | +| Multiple of the same thing | `z_two_databases` | +| All public vars as a component | `x_ns_publics` | +| Logging system lifecycle | `x_log` | +| Visualizing your system | новая, на основе `inspect` + Graphviz | + +- **Starting many keys** — описание расширяем: это не только + тесты, а любая система с несколькими корнями (webserver + + миграции + воркеры). +- **All public vars as a component** — выбранное название для + рецепта про `di/ns-publics`. Точно отражает что фактически + делает функция: собирает один компонент (map var-name → value) + из всех public vars неймспейса. +- **Visualizing your system** — новая страница. Берём пример из + PR #30. Показываем как пропустить вывод `di/inspect` через + Graphviz и получить картинку графа. Ты сам отметил: inspect + важнее логирования, должен быть видимым. + +> автор: + +мне нравится «A namespace as a system» + +--- + +## Reference — 3 страницы + +| Страница | Источник | +|---------------------|---------------------------------------------| +| The Factory protocol| Docstring `protocols.clj` + `dependency_types_test.clj` | +| Inspect | `x_inspect` | +| Middleware types | Из docstrings `core.clj` (новая) | + +- **Factory protocol** — docstring и тест есть; собираем страницу + «когда и как реализовать кастомную фабрику». +- **Inspect** — текущая глава `x_inspect` (377 строк перечисления + форм `:description`). Текст уже неплохой — переносим в Reference, + меняется только роль и навигация. +- **Middleware types** — отвечает на твою фразу «можно даже + в референс добавить типы мидлвар». Перечисляем формы middleware + (function `registry -> key -> Factory`, map, sequence, nil) и + встроенные middlewares: `update-key`, `add-side-dependency`, + `env-parsing`, `ns-publics`, `log`. Каждая — две-три строки + с одной ссылкой на How-to/Tutorial где она объясняется подробно. + +> автор: + +--- + +## Что меняется относительно текущего + +**Добавляется:** +- Новая глава Tutorial: **Why DI?**. +- Новая глава Tutorial: **Interactive development**. +- Новая глава Tutorial: **Composition with `update-key`** + (повышение из Advanced). +- Tutorial: **Multi-arity** повышается из Advanced. +- Новая How-to: **Visualizing your system**. +- Новый раздел: **Reference** (3 страницы). + +**Переезжает:** +- `Inspect` Advanced → Reference. +- `Two databases` Advanced → How-to (Factory выносим в Reference). +- Остальные Advanced → How-to. + +**Не сливаем:** +- `Custom stop` (гл.4) и `Graceful failures` (гл.12) — разные + главы, разные позиции. + +**Не трогаем:** +- Example app, Integrant vs DI, Readme. + +> автор: + +--- + +## Закрытые вопросы (для прозрачности) + +- **V1.** Название гл.10 = «Composition with `update-key`». +- **V2.** Why DI? — переписываю Motivation своими словами, без цитат. +- **V3.** Деобфускацию `gmonit.infra` делаю в Этапе 2, когда + возьмёмся за гл.8. +- **V4.** Renames файлов: `doc/tutorial/01_..` / `doc/how_to/` / + `doc/reference/`. Подтверждено. + +> автор: + +--- + +## Следующий шаг — Этап 2 + +Порядок переписывания: + +1. **Why DI?** — новая глава, нет давления существующего текста. +2. **Your first system** — самая видимая, на свежих принципах. +3. Дальше по порядку Tutorial. +4. How-to и Reference — параллельно по мере необходимости. + +**Артефакт одной сессии:** одна глава, один diff на одном файле. +Возможные исключения — мелкие правки cljdoc.edn и связанные +переименования. + +**Чек-лист для каждой главы** (фиксируется в Этапе 2 в момент +первой переработки): + +1. Сначала ответить «зачем эта глава» одним предложением. +2. Минимальный код-пример, который иллюстрирует ровно одну + мысль главы. +3. Текст вокруг кода — что читатель видит, что узнаёт, + что попробует дальше. +4. Cross-ссылки наружу: где предыдущая мысль, где следующая. +5. Грамматическая вычитка англоязычного текста. + +> автор: diff --git a/doc/cljdoc.edn b/doc/cljdoc.edn index 745d7a31..1e9d7791 100644 --- a/doc/cljdoc.edn +++ b/doc/cljdoc.edn @@ -3,24 +3,28 @@ ["Changelog" {:file "CHANGELOG.md"}] ["Tutorial" ["Base" - ["Intro" {:file "doc/tutorial/a_intro_test.md"}] - ["Dependencies" {:file "doc/tutorial/b_dependencies_test.md"}] - ["Stop" {:file "doc/tutorial/c_stop_test.md"}] - ["Registries" {:file "doc/tutorial/l_registries_test.md"}] - ["Abstractions" {:file "doc/tutorial/m_abstractions_test.md"}] - ["Env" {:file "doc/tutorial/n_env_test.md"}] - ["Data DSL" {:file "doc/tutorial/o_data_dsl_test.md"}] - ["Derive" {:file "doc/tutorial/p_derive_test.md"}] - ["Starting many keys" {:file "doc/tutorial/q_starting_many_keys_test.md"}] - ["Multimethods" {:file "doc/tutorial/r_multimethods_test.md"}]] + ["Your first system" {:file "doc/tutorial/a_intro_test.md"}] + ["Dependencies" {:file "doc/tutorial/b_dependencies_test.md"}] + ["Stopping components" {:file "doc/tutorial/c_stop_test.md"}] + ["Interactive development" {:file "doc/tutorial/d_interactive_development_test.md"}] + ["Registries" {:file "doc/tutorial/l_registries_test.md"}] + ["Abstractions" {:file "doc/tutorial/m_abstractions_test.md"}] + ["Environment variables" {:file "doc/tutorial/n_env_test.md"}] + ["Starting many keys" {:file "doc/tutorial/q_starting_many_keys_test.md"}] + ["Wiring inside data" {:file "doc/tutorial/o_data_dsl_test.md"}] + ["Transforming values" {:file "doc/tutorial/p_derive_test.md"}] + ["Composition with update-key" {:file "doc/tutorial/x_update_key_test.md"}] + ["Graceful failures" {:file "doc/tutorial/y_graceful_stop_test.md"}] + ["Multimethods" {:file "doc/tutorial/r_multimethods_test.md"}]] ["Advanced" + ["Tips" {:file "doc/how_to/tips_test.md"}] ["Add a side dependency" {:file "doc/tutorial/x_add_side_dependency_test.md"}] - ["Update key" {:file "doc/tutorial/x_update_key_test.md"}] ["Log" {:file "doc/tutorial/x_log_test.md"}] ["Inspect" {:file "doc/tutorial/x_inspect_test.md"}] - ["Graceful stop" {:file "doc/tutorial/y_graceful_stop_test.md"}] - ["Multi arity service" {:file "doc/tutorial/y_multi_arity_service_test.md"}] + ["Multi-arity services" {:file "doc/how_to/multi_arity_service_test.md"}] ["Multi system" {:file "doc/tutorial/z_multi_system_test.md"}] ["Two Databases" {:file "doc/tutorial/z_two_databases_test.md"}]]] + ["Reference" + ["Middleware types" {:file "doc/reference/middleware_types_test.md"}]] ["Example app" {:file "doc/example.md"}] ["Integrant vs DI" {:file "doc/integrant.md"}]]} diff --git a/doc/why_di.md b/doc/why_di.md new file mode 100644 index 00000000..11c638ca --- /dev/null +++ b/doc/why_di.md @@ -0,0 +1,139 @@ +# Why DI? + +A typical Clojure project has a few stateful objects — a database +pool, an HTTP server, maybe a cache or a worker queue — surrounded +by many stateless functions. DI starts from the idea that all of +them are first-class components: the stateful objects and the +stateless functions alike. Declaring "this function needs the +datasource" should cost no more than writing a `defn`. + +Everything else follows from that. + +## Dependencies live with the function that uses them + +Each function lists its dependencies in its own argument list: + +```clojure +(defn show-user [{ds ::db/datasource} req] + (jdbc/get-by-id ds (-> req :path-params :id))) +``` + +There is no central wiring file. DI inspects the keys you +destructure and resolves them during start. + +## Lazy initialization + +DI builds only what your root needs, directly or through other +dependencies. A component that nothing uses is never started. + +## Configuration is just components + +Environment variables are the default — a string key in your deps +resolves to an env var. This is plain +[12-factor](https://12factor.net/config) style: a flat namespace of +names and string values, no nested config shape to design or +maintain. If you prefer a config file, read EDN into a map and pass +that map to DI when you start the system. Validation is just a +component that checks its own dependencies — bad configuration +is caught at start, not in production. + +For typed values, qualified keys like `:env.long/PORT` or +`:env.json/SETTINGS` parse the env var on the way in. The keyword +namespaces (`:env.long`, `:env.json`, anything you like) are yours +to define. + +## Subsystems compose without referencing each other + +A subsystem owns its handlers and adds them to a shared registry. +The namespace that owns the registry does not know about the +subsystem. Adding a subsystem means adding a file — no edits to +existing ones. + +Concretely, the users subsystem ships a `registry` function that +hooks itself onto the central routes: + +```clojure +;; app/users.clj +(defn registry [-feature-flags] + [(di/update-key `app.web/routes conj (di/ref `users-routes))]) +``` + +The main system composes registries from every subsystem; `app.web` +never imports `app.users`. A third subsystem is a third file with +its own `registry`. + +## Feature flags come almost for free + +Combine lazy initialization with subsystems that own their wiring, +and feature flags fall out naturally. Flip a flag, and the +subsystem contributes nothing to the registry — the components +behind it stop being built. One binary ships to many environments, +each with a different set of features active. + +## Wiring inside data + +When you describe something in data — reitit routes, scheduler +tables, connection-pool configs — you can put `(di/ref ...)` right +inside the data: + +```clojure +(def route-data + (di/template + [["/users/:id" {:get {:handler (di/ref `show-user)}}]])) +``` + +`di/template` walks the structure on start and replaces each ref +with the built component. + +## Cross-cutting changes don't touch the original code + +Wrap a function with metrics, schema validation, or any decorator +— without editing the function itself. The wrapping lives in the +registry. See +[`di/update-key`](https://cljdoc.org/d/org.clojars.darkleaf/di/CURRENT/api/darkleaf.di.core#update-key). + +## Two kinds of binding + +A stateful component arrives in the function as the built value +itself — no atom to deref, no runtime lookup. To swap one, you +change the registry and restart. + +A stateless service is bound as `(partial #'the-var)`. There is one +level of var indirection, by design: it is exactly what makes live +REPL redefinition work (next section). + +## The system is a value + +`di/start` returns a system root, not a global singleton. Deref it +to get the built component. You can hold a reference to it, start a +second system alongside, pass it around, and stop it independently. +No global registry to reset, no namespace to reload. + + +## Live redefinition works + +Redefine a function with `defn`, and the running system uses the +new version immediately. No restart, no lost state. If you change +a component's dependencies, you do need to restart that component, +but the rest of the system stays alive. + +## Tests share a cached system + +Each test starts its own system and stops it normally. Components +built in one test are reused by the next — DI caches them across +tests via +[`di/->memoize`](https://cljdoc.org/d/org.clojars.darkleaf/di/CURRENT/api/darkleaf.di.core#->memoize). +The whole suite runs as fast as a single system start. Teardown +happens once, at the end. + +## Partial start failures are contained + +If start fails halfway through, DI stops the components it already +started before propagating the error. If those stops also fail, +their exceptions are captured alongside the original — nothing is +lost. + +--- + +The [tutorial](/doc/tutorial/a_intro_test.md) walks through each of +these, one chapter at a time. diff --git a/script/tutorial-to-md.sh b/script/tutorial-to-md.sh index 6326f687..91cdfd3d 100755 --- a/script/tutorial-to-md.sh +++ b/script/tutorial-to-md.sh @@ -1,36 +1,40 @@ #!/usr/bin/env bash set -euo pipefail -src=test/darkleaf/di/tutorial -dst=doc/tutorial - -mkdir -p "$dst" - -for f in "$src"/*.clj; do - name=$(basename "$f" .clj) - awk ' - /^;;/ { - if (in_code) { - print "```"; in_code = 0 - if (blanks == 0) blanks = 1 - } - for (i = 0; i < blanks; i++) print "" - blanks = 0 - sub(/^;; ?/, ""); print; emitted = 1; next - } - /^[[:space:]]*$/ { blanks++; next } - { - if (!in_code) { - if (emitted && blanks == 0) blanks = 1 - for (i = 0; i < blanks; i++) print "" - blanks = 0 - print "```clojure"; in_code = 1 - } else { +convert () { + local src=$1 dst=$2 + mkdir -p "$dst" + for f in "$src"/*.clj; do + [ -e "$f" ] || continue + name=$(basename "$f" .clj) + awk ' + /^;;/ { + if (in_code) { + print "```"; in_code = 0 + if (blanks == 0) blanks = 1 + } for (i = 0; i < blanks; i++) print "" blanks = 0 + sub(/^;; ?/, ""); print; emitted = 1; next + } + /^[[:space:]]*$/ { blanks++; next } + { + if (!in_code) { + if (emitted && blanks == 0) blanks = 1 + for (i = 0; i < blanks; i++) print "" + blanks = 0 + print "```clojure"; in_code = 1 + } else { + for (i = 0; i < blanks; i++) print "" + blanks = 0 + } + print; emitted = 1 } - print; emitted = 1 - } - END { if (in_code) print "```" } - ' "$f" > "$dst/${name}.md" -done + END { if (in_code) print "```" } + ' "$f" > "$dst/${name}.md" + done +} + +convert test/darkleaf/di/tutorial doc/tutorial +convert test/darkleaf/di/how_to doc/how_to +convert test/darkleaf/di/reference doc/reference diff --git a/test/darkleaf/di/tutorial/y_multi_arity_service_test.clj b/test/darkleaf/di/how_to/multi_arity_service_test.clj similarity index 51% rename from test/darkleaf/di/tutorial/y_multi_arity_service_test.clj rename to test/darkleaf/di/how_to/multi_arity_service_test.clj index df817580..a390e54b 100644 --- a/test/darkleaf/di/tutorial/y_multi_arity_service_test.clj +++ b/test/darkleaf/di/how_to/multi_arity_service_test.clj @@ -1,11 +1,15 @@ -;; # Multi arity service +;; # Multi-arity services -(ns darkleaf.di.tutorial.y-multi-arity-service-test +(ns darkleaf.di.how-to.multi-arity-service-test (:require [clojure.test :as t] [darkleaf.di.core :as di])) -;; DI collects dependencies from all arities and only then resolves dependencies. +;; Clojure services often have multiple arities — one with defaults +;; that calls into a richer one. DI handles this by collecting +;; dependencies from every arity, then resolving them once before +;; the service is bound. Each arity receives the same fully-resolved +;; dependency map. (defn multi-arity-service ([{a `a, :as deps}] @@ -15,9 +19,12 @@ ([deps arg1 arg2] [::result deps arg1 arg2])) +;; The 1-arg arity declares `a`; the 2-arg arity declares `b`. DI +;; reads both and resolves both — when you call `(s)`, you still +;; get a map containing `a` and `b`. + (t/deftest multi-arity-service-test (with-open [s (di/start `multi-arity-service {`a :a, `b :b})] - ;; each arity gets all the dependencies (t/is (= [::result {`a :a, `b :b} :a1 :a2] (s))) (t/is (= [::result {`a :a, `b :b} :arg1 :a2] (s :arg1))) (t/is (= [::result {`a :a, `b :b} :arg1 :arg2] (s :arg1 :arg2))))) diff --git a/test/darkleaf/di/how_to/tips_test.clj b/test/darkleaf/di/how_to/tips_test.clj new file mode 100644 index 00000000..fffc2a20 --- /dev/null +++ b/test/darkleaf/di/how_to/tips_test.clj @@ -0,0 +1,43 @@ +;; # Tips + +;; TODO: collection of small DI tricks and lesser-known features. +;; More to come — add new ones here as we run into them. + +(ns darkleaf.di.how-to.tips-test + (:require + [clojure.test :as t] + [darkleaf.di.core :as di])) + +;; ## `::di/stop` infers `:component` + +;; You don't need to attach `{::di/kind :component}` if the +;; function already has `::di/stop` metadata. DI treats any +;; function with a stop hook as a component — there is no other +;; reasonable interpretation, since services don't have a built +;; value to stop. + +(defn resource + {::di/stop #(reset! % :stopped)} + [] + (atom :running)) + +(t/deftest stop-implies-component-test + (let [root (di/start `resource) + a @root] + (t/is (= :running @a)) + (di/stop root) + (t/is (= :stopped @a)))) + +;; ## Group registries into one argument + +;; A seqable value counts as a single registry argument — handy +;; when registries come from helper functions and you'd otherwise +;; need `(apply di/start ...)`. + +(t/deftest grouped-registry-test + ;; instead of: + ;; (di/start ::root {::root :first} {::root :replacement}) + ;; ... pass them grouped: + (with-open [r (di/start ::root [{::root :first} + {::root :replacement}])] + (t/is (= :replacement @r)))) diff --git a/test/darkleaf/di/reference/middleware_types_test.clj b/test/darkleaf/di/reference/middleware_types_test.clj new file mode 100644 index 00000000..206ac60a --- /dev/null +++ b/test/darkleaf/di/reference/middleware_types_test.clj @@ -0,0 +1,11 @@ +;; # Middleware types + +(ns darkleaf.di.reference.middleware-types-test + (:require + [clojure.test :as t])) + +;; TODO: describe the registry-middleware abstraction +;; (`registry -> key -> Factory`) and list the built-in +;; middlewares: `di/update-key`, `di/env-parsing`, +;; `di/add-side-dependency`, `di/log`, `di/ns-publics`, +;; map form, seqable form. diff --git a/test/darkleaf/di/tutorial/a_intro_test.clj b/test/darkleaf/di/tutorial/a_intro_test.clj index 18539c55..9a61b689 100644 --- a/test/darkleaf/di/tutorial/a_intro_test.clj +++ b/test/darkleaf/di/tutorial/a_intro_test.clj @@ -1,4 +1,4 @@ -;; # Intro +;; # Your first system (ns darkleaf.di.tutorial.a-intro-test (:require @@ -7,39 +7,43 @@ (:import (java.time Instant))) -;; Let's start. -;; In this chapter I'll show you how to deal with components. +;; By the end of this chapter you can start a small system, stop +;; it, and see the difference between a component and a service. +;; The rest of the tutorial builds on these terms. -;; ## Trivial system -;; The following test describes the most trivial system that -;; contains the most trivial component. +;; ## The smallest system + +;; A system is one or more components connected by their +;; dependencies. The smallest one has a single component — a +;; trivial value stored in a var. (def a ::a) -;; `root` is a system root. -;; To get root's value deref it. -;; To stop a system use `di/stop`. +;; `di/start` builds the system. It takes a key, looks it up, and +;; returns the system root. Deref the root with `@` to get the +;; built value. `di/stop` shuts the system down. (t/deftest a-test (let [root (di/start `a)] (t/is (= ::a @root)) (di/stop root))) -;; ## AutoCloseable +;; ## Stopping safely -;; A root implements `AutoCloseable` -;; so in tests we should use `with-open` macro -;; for properly stopping. +;; The root implements `AutoCloseable`, so in tests use `with-open` +;; instead of calling `di/stop` by hand. If a test fails midway, +;; the system is still stopped. (t/deftest a'-test (with-open [root (di/start `a)] (t/is (= ::a @root)))) -;; ## Component +;; ## Components -;; A component definition is a function of 0 or 1 arity -;; with `{::di/kind :component}` meta. +;; A component is a function of zero or one argument with +;; `{::di/kind :component}` metadata. DI calls it once during start +;; and uses the returned value. (defn b {::di/kind :component} @@ -50,13 +54,10 @@ (with-open [root (di/start `b)] (t/is (inst? @root)))) -;; ## Dependencies - -;; To define a component that depends on other components, -;; define a function of one argument. -;; DI will parse associative destructuring to get dependencies of the component. -;; We'll consider component dependencies in the next chapter. -;; But now we will use placeholder. +;; The argument, when present, carries the component's +;; dependencies. DI reads the destructuring map to figure out what +;; to inject. Declaring real dependencies is the next chapter; for +;; now we just use a placeholder name. (defn c {::di/kind :component} @@ -69,18 +70,23 @@ ;; ## Services -;; A service is a function with or without dependencies. +;; A service is a plain `defn` — no metadata. DI does not call it +;; during start; the function itself is the component. (defn d [] ::d) -;; `root` is a wrapper, and it implements `clojure.lang.IFn`, just like `clojure.lang.Var`. -;; So you can just call `root`. +;; The root implements `clojure.lang.IFn`, so you can call it +;; directly — `(root)` invokes the underlying function, just like +;; you would invoke a var. (t/deftest d-test (with-open [root (di/start `d)] (t/is (= ::d (@root) (root))))) +;; A service can also take dependencies. As with a component, the +;; first argument is the dependency map (placeholder for now). + (defn d* [-deps] ::d) @@ -88,6 +94,9 @@ (with-open [root (di/start `d*)] (t/is (= ::d (@root) (root))))) +;; Arguments after the dependency map are the service's own +;; arguments, supplied by the caller. + (defn e [-deps arg] [::e arg]) @@ -95,23 +104,7 @@ (with-open [root (di/start `e)] (t/is (= [::e 42] (root 42))))) -;; ## Interactive Development - -;; You don't need to restart the whole system if you redefine a service. -;; Just redefine a Var. -;; It's very helpful for interactive development. - -;; It does not work if you change definition of dependencies, -;; so in this case you have to restart the system. - -;; The new implementation of a service will receive the same dependencies. -;; To check that, I have to look a little ahead and define component with a dependency. -;; As I said we consider deps in the next chapter. +;; That's the vocabulary: system, root, components, and services. +;; The next chapter wires components together through real +;; dependencies. -(t/deftest f-test - (defn f [{x ::x} arg] - [::f x arg]) - (with-open [root (di/start `f {::x :x})] - (defn f [deps arg] - [::new-f (deps ::x) arg]) - (t/is (= [::new-f :x 42] (root 42))))) diff --git a/test/darkleaf/di/tutorial/b_dependencies_test.clj b/test/darkleaf/di/tutorial/b_dependencies_test.clj index 63edd573..180a9bc9 100644 --- a/test/darkleaf/di/tutorial/b_dependencies_test.clj +++ b/test/darkleaf/di/tutorial/b_dependencies_test.clj @@ -3,22 +3,25 @@ (ns darkleaf.di.tutorial.b-dependencies-test (:require [clojure.test :as t] - [darkleaf.di.core :as di]) - (:import - (clojure.lang ExceptionInfo))) + [darkleaf.di.core :as di] + [darkleaf.di.utils :as u])) -;; DI uses associative destructuring syntax to define dependencies of a component. -;; https://clojure.org/guides/destructuring#_associative_destructuring +;; The previous chapter built a single component on its own. Real +;; systems are graphs: components depend on other components. This +;; chapter shows how a component declares its dependencies and how +;; DI resolves them at start. -;; There is a mapping between keys and components. -;; A key can be symbol, keyword, or string. -;; In this chapter we'll use only symbols. +;; ## Declaring dependencies -;; If we use symbols DI will try to resolve a component's var. +;; DI uses Clojure's +;; [associative destructuring](https://clojure.org/guides/destructuring#_associative_destructuring) +;; to read a component's dependencies. Keys can be symbols, +;; keywords, or strings — this chapter uses only symbols, which DI +;; resolves to vars by name. -;; In the following example the `root` component depends on -;; the `a` and `b` and the `b` is optional. -;; You also can get all component dependencies by the `deps` binding. +;; `root` below depends on `a` and `b`. `b` is optional via `:or` +;; with `::default` as fallback. The full dependency map is also +;; available through `:as deps`. (defn root {::di/kind :component} @@ -34,17 +37,33 @@ (with-open [root (di/start `root)] (t/is (= [:root ::a ::default {`a ::a}] @root)))) -;; `di/start` can accept additional arguments. -;; In the following example the argument is a map registry. -;; I use it to define local keys. -;; In general they are middlewares but I'll describe it later. +;; Two equivalent forms in the destructuring map: ``{a `a}`` binds +;; the value of key `` `a `` to the local `a`. `::syms [b]` is a +;; shorthand for several symbols at once. Use whichever reads +;; better. + +;; ## Substituting a dependency + +;; `di/start` accepts a second argument — a map that supplies or +;; overrides values by key: (t/deftest root-with-extra-deps-test (with-open [root (di/start `root {`b ::b})] (t/is (= [:root ::a ::b {`a ::a `b ::b}] @root)))) -;; Dependencies are required by default. -;; There is no definition of `a'` so DI will throw an exception. +;; This map is called a *registry*. Use it to override what DI +;; would otherwise resolve from a var — a fake datasource in +;; tests, a different implementation in dev, and so on. +;; Registries are covered in detail in +;; [Registries](/doc/tutorial/l_registries_test.md). + +;; ## Required by default + +;; Dependencies are required unless `:or` declares a default. A +;; missing required dependency makes `di/start` throw. The +;; exception carries enough info to find the gap: the failure +;; `:type` and a `:stack` of keys DI was resolving — from the +;; missing key (head) up through its parents to the root. (defn root' {::di/kind :component} @@ -52,6 +71,12 @@ [::root a]) (t/deftest root'-test - (t/is (thrown-with-msg? ExceptionInfo - #"Missing dependency darkleaf.di.tutorial.b-dependencies-test/a'" - (di/start `root')))) + (let [ex (u/catch-some (di/start `root'))] + (t/is (= "Missing dependency darkleaf.di.tutorial.b-dependencies-test/a'" + (ex-message ex))) + (t/is (= {:type ::di/missing-dependency + :stack [`a' `root']} + (ex-data ex))))) + +;; The next chapter covers how a component cleans up when the +;; system stops. diff --git a/test/darkleaf/di/tutorial/c_stop_test.clj b/test/darkleaf/di/tutorial/c_stop_test.clj index 1c812ed6..e1b533db 100644 --- a/test/darkleaf/di/tutorial/c_stop_test.clj +++ b/test/darkleaf/di/tutorial/c_stop_test.clj @@ -1,12 +1,22 @@ -;; # Stop +;; # Stopping components (ns darkleaf.di.tutorial.c-stop-test (:require [clojure.test :as t] [darkleaf.di.core :as di])) -;; To stop a component, you should teach DI how to do it. -;; Use `::di/stop` to define a stop function. +;; By default, a component is built once at start and discarded on +;; stop — DI does nothing else with it. When a component owns a +;; resource (a connection, a thread pool, a file handle), attach +;; a stop function via metadata. DI calls it with the built value +;; when the system shuts down. + +;; ## Declaring a stop function + +;; `::di/stop` is just another function attached as metadata. It +;; receives the built value and its return value is ignored. Below, +;; the component returns an atom, and the stop function flips it to +;; `true` — the test asserts the flip happened. (defn root {::di/stop #(reset! % true)} @@ -19,12 +29,24 @@ (t/is (= false @@root))) (t/is @*stopped?))) -;; In most cases, a component will be a Java class. -;; To prevent reflection calls use `memfn` +;; ## Stopping Java objects + +;; In real systems, a stateful component is often a Java object — +;; a connection pool, a server, a queue — with a `close` or +;; `shutdown` method. Use a qualified method value (Clojure 1.12+) +;; or `memfn` to call it without reflection: + ;; ```clojure -;; (defn- connection-manager -;; {::di/stop (memfn ^AutoCloseable close)} -;; [{max-conn :env.long/CONNECTION_MANAGER_MAX_CONN -;; :or {max-conn 50}}] -;; (ConnectionManager. max-conn)) +;; (defn connection-pool +;; {::di/stop ConnectionPool/.close} +;; [{max-conn :env.long/MAX_CONN}] +;; (ConnectionPool. max-conn)) +;; +;; (defn connection-pool +;; {::di/stop (memfn ^ConnectionPool close)} +;; [{max-conn :env.long/MAX_CONN}] +;; (ConnectionPool. max-conn)) ;; ``` + +;; The next chapter covers the REPL workflow — redefining functions +;; on a running system without restarting. diff --git a/test/darkleaf/di/tutorial/d_interactive_development_test.clj b/test/darkleaf/di/tutorial/d_interactive_development_test.clj new file mode 100644 index 00000000..5261b838 --- /dev/null +++ b/test/darkleaf/di/tutorial/d_interactive_development_test.clj @@ -0,0 +1,55 @@ +;; # Interactive development + +(ns darkleaf.di.tutorial.d-interactive-development-test + (:require + [clojure.test :as t] + [darkleaf.di.core :as di])) + +;; In Clojure you grow a program by evaluating new code into a +;; running process. State — db connections, server threads, caches +;; — survives the eval, but the functions that capture that state +;; can grow stale. Stuart Sierra's *Reloaded Workflow* (2013) answers +;; this by stopping the system, reloading namespaces, and starting +;; a new one. DI offers a smaller-scope answer for the common case: +;; you don't have to restart anything to update a service. + +;; ## Redefining a service + +;; A service is bound as `(partial #'the-var deps)`. There is one level +;; of var indirection between the running system and the function +;; body. When you redefine the var with `defn`, the next call goes +;; through the new function immediately — no restart needed. + +(t/deftest redefine-service-test + (defn f [{x ::x} arg] + [::f x arg]) + (with-open [root (di/start `f {::x :x})] + (t/is (= [::f :x 42] (root 42))) + ;; redefine f while the system is running + (defn f [deps arg] + [::new-f (deps ::x) arg]) + ;; the running root picks up the new implementation + (t/is (= [::new-f :x 42] (root 42))))) + +;; Notice the new implementation received the same dependency map +;; (`{::x :x}`) the system was started with. DI does not recompute +;; the wiring on redef. + +;; ## When a restart is needed + +;; The var indirection only covers the function body. The +;; dependency graph itself was decided at start. If you: +;; +;; - add a new dependency key, +;; - rename or remove one, +;; - change `:required` to `:optional`, +;; - turn a `defn` into `{::di/kind :component}` or back, +;; +;; the running system keeps the old wiring. Stop and start again +;; to pick up the change. + +;; The same applies to components — their built value is what the +;; system holds. Redefining the var does not rebuild the component. + +;; The next chapter zooms in on the registry: how to override +;; dependencies, stack overrides, and what last-wins means. diff --git a/test/darkleaf/di/tutorial/l_registries_test.clj b/test/darkleaf/di/tutorial/l_registries_test.clj index 680e548b..b80c16b2 100644 --- a/test/darkleaf/di/tutorial/l_registries_test.clj +++ b/test/darkleaf/di/tutorial/l_registries_test.clj @@ -5,7 +5,15 @@ [clojure.test :as t] [darkleaf.di.core :as di])) -;; Here we use undefined dependencies. +;; Earlier chapters used `di/start` with a map second argument +;; without naming it. That map is a *registry*. A registry tells +;; DI what to use for a given key — overriding what DI would +;; otherwise resolve from a var, or supplying a value for a key +;; that has no var at all. + +;; The component below declares two dependencies that DI cannot +;; resolve on its own — `dep-a` and `dep-b` have no vars. The +;; registry fills them in. (defn value {::di/kind :component} @@ -13,29 +21,50 @@ dep-b `dep-b}] [:value dep-a dep-b]) -;; To locally define or redefine a dependency we should use registries. +;; ## A map of values by key + +;; The simplest registry is a map. Each entry maps a key to the +;; value DI should use. Any key can be overridden — including the +;; root key itself. -(t/deftest map-registry +(t/deftest map-registry-test + ;; supply two undefined deps (with-open [root (di/start `value {`dep-a :a `dep-b :b})] (t/is (= [:value :a :b] @root))) - + ;; replace the root with a literal value (with-open [root (di/start `value {`value :replacement})] - (t/is (= :replacement @root))) + (t/is (= :replacement @root)))) + +;; ## Stacking registries — last wins + +;; `di/start` takes any number of registries after the key. They +;; stack: a key is resolved in the right-most registry that +;; defines it. +(t/deftest stacked-registries-test + ;; two registries together (with-open [root (di/start `value {`dep-a :a} {`dep-b :b})] (t/is (= [:value :a :b] @root))) - - ;; last wins + ;; later wins (with-open [root (di/start `value {`dep-a :a `dep-b :b} {`dep-a :a' `dep-b :b'})] (t/is (= [:value :a' :b'] @root)))) +;; ## Grouping registries with a sequence + +;; To avoid splicing with `apply`, a seqable value (see +;; `clojure.core/seqable?`) counts as a single registry. DI walks +;; the sequence as if you had passed each entry separately. -;; To avoid using `(apply di/start ...)`, -;; we can use a seqable value as a single registry. -;; See `clojure.core/seqable?`. -(t/deftest seqable-registry +(t/deftest seqable-registry-test (with-open [root (di/start `value [{`dep-a :a} [{`dep-b :b}]])] (t/is (= [:value :a :b] @root)))) + +;; The map form is one of several registry shapes — see +;; [Middleware types](/doc/reference/middleware_types_test.md) +;; for the full picture. The tutorial only needs the map form. + +;; The next chapter introduces keyword keys — a way to decouple +;; a component from any specific var. diff --git a/test/darkleaf/di/tutorial/m_abstractions_test.clj b/test/darkleaf/di/tutorial/m_abstractions_test.clj index 3350871d..6d548bf1 100644 --- a/test/darkleaf/di/tutorial/m_abstractions_test.clj +++ b/test/darkleaf/di/tutorial/m_abstractions_test.clj @@ -3,11 +3,25 @@ (ns darkleaf.di.tutorial.m-abstractions-test (:require [clojure.test :as t] - [darkleaf.di.core :as di])) + [darkleaf.di.core :as di] + [darkleaf.di.utils :as u])) -;; For some reasons, we may want to not depend on specific vars. -;; In this case, use keywords instead of symbols to define dependencies. -;; Later in the main function you will be able to bind all parts of your application. +;; Symbol keys resolve to vars — when DI sees `` `foo `` in a +;; destructuring map, it looks up the var `foo` in the current +;; namespace. That's the default and it covers most code: you are +;; wiring functions together by their names. + +;; Sometimes the dependency does not belong to any one namespace — +;; a database connection, a session source, a config map. There +;; is no var to point at; the value comes from outside the code. +;; For these, use a keyword key. DI does not try to resolve it; +;; the registry must supply it. + +;; ## A worked example + +;; In the example below, `::datasource` and `::session` are +;; abstractions — they have no vars. The `ring-handler` does not +;; care where they come from. At start, the registry binds them. (defn get-user [{ds ::datasource} id] (ds id)) @@ -25,3 +39,65 @@ ::session {:user-id 1}})] (t/is (= {:status 200 :body "Hi, John!"} (root {}))))) + +;; Notice the mix: `get-user` and `get-current-user` are symbol +;; deps (they have real vars). `::datasource` and `::session` are +;; keyword deps (no vars; supplied at start). Both kinds sit side +;; by side in the same destructuring map. + +;; ## When to pick which + +;; **Symbol** points at a specific var. You are naming the +;; implementation directly. This is the default for code you +;; write — no extra wiring needed; DI resolves the var. + +;; **Keyword** does not point at anything. It names an abstract +;; role; the registry decides which implementation fills it. Use +;; a keyword when you have explicitly decided to abstract a +;; dependency — most commonly in a reusable library or an +;; internal module that declares what it needs without naming +;; the implementation, or when the role itself has no canonical +;; default that should live in code. + +;; Both kinds can be overridden by a registry — swap-ability is +;; not the deciding factor. The choice is whether the dependency +;; has a default implementation tied to a var, or is abstract by +;; design. + +;; ## Validating the abstraction + +;; Because a keyword has no var, the registry can supply anything +;; for it. If you want a contract check — must be callable, must +;; satisfy a protocol, must match a spec — declare a same-named +;; component that validates the registry value and returns it. +;; Downstream code depends on the wrapper symbol, not the raw +;; keyword. + +(defn user-repo + {::di/kind :component} + [{repo ::user-repo}] + (when-not (ifn? repo) + (throw (ex-info "::user-repo must be callable" {:value repo}))) + repo) + +;; Good value goes through: + +(t/deftest checked-user-repo-test + (with-open [root (di/start `user-repo + {::user-repo #(str "user-" %)})] + (t/is (= "user-7" (@root 7))))) + +;; A bad value fails the system at start, before any code touches +;; it. The original error is wrapped as `::di/build-failure`; the +;; original is available via `ex-cause`. + +(t/deftest checked-user-repo-failure-test + (let [ex (u/catch-some (di/start `user-repo {::user-repo 42}))] + (t/is (= ::di/build-failure (-> ex ex-data :type))) + (t/is (= "::user-repo must be callable" + (-> ex ex-cause ex-message))) + (t/is (= {:value 42} + (-> ex ex-cause ex-data))))) + +;; The next chapter introduces the third kind of key: strings, +;; for pulling in environment variables. diff --git a/test/darkleaf/di/tutorial/n_env_test.clj b/test/darkleaf/di/tutorial/n_env_test.clj index ad08b2af..d412d37b 100644 --- a/test/darkleaf/di/tutorial/n_env_test.clj +++ b/test/darkleaf/di/tutorial/n_env_test.clj @@ -1,12 +1,20 @@ -;; # Env +;; # Environment variables (ns darkleaf.di.tutorial.n-env-test (:require [clojure.test :as t] [darkleaf.di.core :as di])) -;; Like symbols and keywords, you can also use strings for keys. -;; String keys are resolved into values of environment variables. +;; Symbols resolve to vars; keywords to abstractions in the +;; registry. The third kind of key — a string — resolves to an +;; environment variable. This is how you plug a system into the +;; environment it runs in: container env, .env file, shell, CI +;; secrets. + +;; ## String keys → env vars + +;; Declare a string in the destructuring map. DI looks the value +;; up from `System/getenv`. (defn root {::di/kind :component} @@ -19,12 +27,16 @@ (with-open [root (di/start `root)] (t/is (= [:root PATH] @root)))) -;; As of 2.3.0, there is `di/env-parsing` registry middleware -;; to parse values of environment variables. -;; You can define a dependency of env as a string key like \"PORT\", -;; and its value will be a string. -;; With this middleware, you can define it as a qualified keyword like :env.long/PORT, -;; and its value will be a number. +;; The value is always a string — the way the OS sees it. To get +;; numbers, booleans, JSON, edn, parse them on the way in. + +;; ## Typed values via `di/env-parsing` + +;; Pass `(di/env-parsing ...)` to `di/start` and you can depend +;; on a qualified keyword like `:env.long/PORT`. DI looks up the +;; env var by name (`PORT`) and passes its string through the +;; parser registered for the keyword namespace (`:env.long` → +;; `parse-long`). (defn jetty {::di/kind :component} @@ -33,14 +45,30 @@ [:jetty port]) (t/deftest jetty-test + ;; PORT is not set — :or default kicks in (with-open [jetty (di/start `jetty (di/env-parsing {:env.long parse-long}))] (t/is (= [:jetty 8080] @jetty))) + ;; PORT supplied as a string in a registry — parsed to a number (with-open [jetty (di/start `jetty (di/env-parsing :env.long parse-long) {"PORT" "8081"})] (t/is (= [:jetty 8081] @jetty)))) +;; Both the kwargs form (`:env.long parse-long`) and the map form +;; (`{:env.long parse-long}`) work. Register as many keyword +;; namespaces as you need: `:env.long`, `:env.bool`, `:env.json`, +;; anything. + +;; (`di/env-parsing` is one of several registry shapes — see +;; [Middleware types](/doc/reference/middleware_types_test.md) +;; for the wider picture.) + +;; ## Required vs optional + +;; An env-typed dependency without `:or` is required. If the +;; underlying env var is missing, `di/start` throws. + (defn required-env {::di/kind :component} [{enabled :env.bool/ENABLED}] @@ -49,24 +77,32 @@ (defn optional-env {::di/kind :component} [{enabled :env.bool/ENABLED - :or {enabled true}}] + :or {enabled true}}] [:enabled enabled]) (t/deftest env-test + ;; required, ENABLED is not set → fails at start (t/is (thrown? clojure.lang.ExceptionInfo (di/start `required-env (di/env-parsing {:env.bool #(= "true" %)})))) + ;; required, ENABLED supplied via registry (with-open [sys (di/start `required-env (di/env-parsing {:env.bool #(= "true" %)}) {"ENABLED" "false"})] (t/is (= [:enabled false] @sys))) + ;; optional, ENABLED not set → :or default (with-open [sys (di/start `optional-env (di/env-parsing {:env.bool #(= "true" %)}))] (t/is (= [:enabled true] @sys))) + ;; optional, ENABLED supplied → overrides the default (with-open [sys (di/start `optional-env (di/env-parsing {:env.bool #(= "true" %)}) {"ENABLED" "false"})] (t/is (= [:enabled false] @sys)))) + +;; The next chapter shows how to wire components into plain data +;; structures — reitit routes, scheduler tables — using +;; `di/template` and `di/ref`. diff --git a/test/darkleaf/di/tutorial/o_data_dsl_test.clj b/test/darkleaf/di/tutorial/o_data_dsl_test.clj index 750b2178..cafd27e9 100644 --- a/test/darkleaf/di/tutorial/o_data_dsl_test.clj +++ b/test/darkleaf/di/tutorial/o_data_dsl_test.clj @@ -1,27 +1,65 @@ -;; # Data DSL +;; # Wiring inside data (ns darkleaf.di.tutorial.o-data-dsl-test (:require [clojure.test :as t] [darkleaf.di.core :as di])) -;; It is common to use data-DSLs in Clojure, such as reitit routing, -;; and DI offers tools to handle them easily. -;; Here they are: `di/template` and `di/ref`. +;; Many Clojure libraries are configured via data — reitit routes, +;; scheduler tables, connection-pool maps. DI lets you embed +;; `(di/ref ...)` directly inside that data; `di/template` walks +;; the structure on start and replaces each ref with the value it +;; points at. -;; You can also use `di/opt-ref` for optional dependencies. -;; If there is no defined key opt-ref resolves to nil. +;; ## Embedding refs in data -(def route-data +;; Below, `routes` is reitit-style data with handler references at +;; each leaf. The handlers are real services with their own +;; dependencies. At start, DI resolves every `di/ref` — the +;; handlers in the produced data already have `::datasource` +;; baked in. + +(defn list-users [{ds ::datasource} -req] + (->> ds vals sort vec)) + +(defn show-user [{ds ::datasource} req] + (ds (-> req :path-params :id))) + +(defn create-user [{ds ::datasource} -req] + {:status :created, :existing-count (count ds)}) + +(def routes + (di/template + [["/users" {:get (di/ref `list-users) + :post (di/ref `create-user)}] + ["/users/:id" {:get (di/ref `show-user)}]])) + +(t/deftest routes-test + (di/with-open [[routes list-users create-user show-user] + (di/start [`routes `list-users `create-user `show-user] + {::datasource {"1" "Alice", "2" "Bob"}})] + (t/is (= [["/users" {:get list-users + :post create-user}] + ["/users/:id" {:get show-user}]] + routes)))) + +;; `di/template` walks any nested structure — vectors, maps, sets +;; — and resolves every `di/ref` it finds. Everything else passes +;; through unchanged. + +;; ## Optional refs + +;; `di/opt-ref` resolves to `nil` when the key has no value. +;; Useful when a data DSL has slots that may or may not be filled. + +(def maybe-handler (di/template - [["/" {:get {:handler (di/ref `root-handler)}}] - ["/news" {:get {:handler (di/ref `news-handler)}}]])) - -(t/deftest template-test - (letfn [(root-handler [req]) - (news-handler [req])] - (with-open [root (di/start `route-data {`root-handler root-handler - `news-handler news-handler})] - (t/is (= [["/" {:get {:handler root-handler}}] - ["/news" {:get {:handler news-handler}}]] - @root))))) + {:get (di/opt-ref `nonexistent-handler)})) + +(t/deftest opt-ref-test + (with-open [root (di/start `maybe-handler)] + (t/is (= {:get nil} @root)))) + +;; The next chapter covers `di/derive` — transforming a built +;; value with a function, useful for parsing, normalizing, or +;; wrapping the result of another dependency. diff --git a/test/darkleaf/di/tutorial/p_derive_test.clj b/test/darkleaf/di/tutorial/p_derive_test.clj index 37bd7574..23dfebdf 100644 --- a/test/darkleaf/di/tutorial/p_derive_test.clj +++ b/test/darkleaf/di/tutorial/p_derive_test.clj @@ -1,33 +1,53 @@ -;; # Derive +;; # Transforming values (ns darkleaf.di.tutorial.p-derive-test (:require [clojure.test :as t] [darkleaf.di.core :as di])) -;; In some cases, your components may have a complex structure or require transformation. -;; You can use `di/derive` to transform a component. +;; Sometimes the value DI builds for a key is not quite what +;; downstream code expects: an env var arrives as a string but +;; you want a number, a templated list contains nils you want +;; filtered out. `di/derive` builds a value from another key and +;; runs a function over the result. Shape: -;; The first way +;; ```clojure +;; (di/derive source-key f arg1 arg2 ...) +;; ;; ≡ (f source-value arg1 arg2 ...) +;; ``` -(defn port - {::di/kind :component} - [{port "PORT"}] - (parse-long port)) +;; ## A typed env var + +;; The simplest case: parse an env var. + +(def port (di/derive "PORT" parse-long)) (t/deftest port-test (with-open [root (di/start `port {"PORT" "8080"})] (t/is (= 8080 @root)))) +;; Same effect as defining a one-line component: -;; The second way - -(def port' (di/derive "PORT" parse-long)) +(defn port' + {::di/kind :component} + [{port "PORT"}] + (parse-long port)) (t/deftest port'-test (with-open [root (di/start `port' {"PORT" "8080"})] (t/is (= 8080 @root)))) +;; Use whichever reads better. For env vars specifically, +;; [`di/env-parsing`](/doc/tutorial/n_env_test.md) is usually +;; nicer — it registers a parser once for a whole keyword +;; namespace (`:env.long`, `:env.json`). `di/derive` is the +;; general tool, useful when the transformation does not fit the +;; env-parsing pattern. + +;; ## Post-processing a template + +;; `di/derive` shines on top of a `di/template`. Here we build a +;; list of optional refs and filter out the nils: (def -box (di/template [(di/opt-ref ::a) (di/opt-ref ::b) @@ -37,3 +57,7 @@ (t/deftest box-test (with-open [root (di/start `box {::b :b})] (t/is (= [:b] @root)))) + +;; The next chapter is what the whole tutorial has been pointing +;; at: composition through `di/update-key`. With it, modules +;; assemble into a system without naming each other. diff --git a/test/darkleaf/di/tutorial/q_starting_many_keys_test.clj b/test/darkleaf/di/tutorial/q_starting_many_keys_test.clj index 3fd1b6b9..c375f386 100644 --- a/test/darkleaf/di/tutorial/q_starting_many_keys_test.clj +++ b/test/darkleaf/di/tutorial/q_starting_many_keys_test.clj @@ -5,29 +5,39 @@ [darkleaf.di.core :as di] [clojure.test :as t])) -;; The standard `with-open` does not support destructuring in bindings. -;; Use `di/with-open` to handle resources with destructuring support. +;; A single `di/start` can bring up several components at once — +;; a webserver and a worker queue and a scheduler in production, +;; or a few independent components a test wants to poke at. +;; Rather than writing an explicit root component that pulls all +;; of them in, hand `di/start` a vector or a map of keys +;; directly. The returned root supports the matching kind of +;; destructuring. (def a :a) (def b :b) -(t/deftest verbose-test - (di/with-open [[a b] (di/start ::root {::root (di/template [(di/ref `a) (di/ref `b)])})] - (t/is (= :a a)) - (t/is (= :b b)))) +;; ## Vector — Indexed root -;; The root container implements `clojure.lang.Indexed` -;; so you can use destructuring without derefing the root. +;; A vector of keys produces a root that implements +;; `clojure.lang.Indexed`. Sequence destructuring works directly +;; — use `di/with-open` (a drop-in replacement for +;; `clojure.core/with-open` that supports destructuring): (t/deftest indexed-test (di/with-open [[a b] (di/start [`a `b])] (t/is (= :a a)) (t/is (= :b b)))) -;; The root container implements `clojure.lang.ILookup` -;; so you can use destructuring without derefing the root. +;; ## Map — ILookup root + +;; A map of label → key produces a root that implements +;; `clojure.lang.ILookup`. Associative destructuring works: (t/deftest lookup-test (di/with-open [{:keys [a b]} (di/start {:a `a :b `b})] (t/is (= :a a)) (t/is (= :b b)))) + +;; The next chapter shows how to wire components into plain data +;; — reitit routes, scheduler tables — with `di/template` and +;; `di/ref`. diff --git a/test/darkleaf/di/tutorial/x_update_key_test.clj b/test/darkleaf/di/tutorial/x_update_key_test.clj index 8a6addb2..d75acd9e 100644 --- a/test/darkleaf/di/tutorial/x_update_key_test.clj +++ b/test/darkleaf/di/tutorial/x_update_key_test.clj @@ -1,4 +1,4 @@ -;; # Update key +;; # Composition with `update-key` (ns darkleaf.di.tutorial.x-update-key-test (:require @@ -6,26 +6,25 @@ [darkleaf.di.core :as di])) ;; `di/update-key` rewires an existing key: the original value is -;; built first, your function transforms it, and the result is what -;; every dependent sees. Multiple `update-key` calls on the same -;; target apply in registration order — each transforms the result of -;; the previous. - -;; This is the main tool for composing across namespaces — the -;; namespace that owns a key doesn't need to know about the modules -;; that decorate or extend it. It covers the two cases Integrant has -;; no clean answer for: AOP-style wrappers around components and -;; shared registries assembled from independent modules (see -;; [Integrant vs DI](/doc/integrant.md)). +;; built first, your function transforms it, and the result is +;; what every dependent sees. Multiple `update-key` calls on the +;; same target apply in registration order — each transforms the +;; result of the previous. + +;; This is the main tool for cross-namespace composition: the +;; namespace that owns a key does not need to know about the +;; modules that decorate or extend it. Two patterns below cover +;; most uses — wrapping a value with extra behaviour, and +;; extending a shared collection. ;; ## Decorate the built value -;; `(di/update-key target f & args)` applies `f` to the built value -;; of `target`, threading it as the first argument. The classic case -;; is the decorator pattern: wrap the original in something with the -;; same shape that delegates to it, adding behaviour. In Clojure this -;; is usually a higher-order `wrap-X` — takes the thing, returns a -;; wrapped thing of the same kind. +;; `(di/update-key target f & args)` applies `f` to the built +;; value of `target`, threading it as the first argument. The +;; classic case is the decorator pattern: wrap the original in +;; something with the same shape that delegates to it, adding +;; behaviour. In Clojure this is usually a higher-order `wrap-X` +;; — takes the thing, returns a wrapped thing of the same kind. (defn handler [-deps req] {:status 200 :body (:uri req)}) @@ -46,10 +45,10 @@ ;; ## Extend a collection ;; Any argument after `f` is itself a factory and gets built. This -;; lets each module attach itself to a shared registry: it owns its -;; handler and the route entry that wires the handler in, then hooks -;; the entry onto `routes` with `di/ref`. The namespace that defines -;; `routes` never references any of the modules. +;; lets each module attach itself to a shared registry: it owns +;; its handler and the route entry that wires the handler in, +;; then hooks the entry onto `routes` with `di/ref`. The namespace +;; that defines `routes` never references any of the modules. (defn user-handler [-deps -req] :user) @@ -73,3 +72,11 @@ ["/orders" :order]] (for [[path handler] @root] [path (handler :req)]))))) + +;; (Under the hood `di/update-key` is a registry middleware — see +;; [Middleware types](/doc/reference/middleware_types_test.md) +;; for what that means. For everyday use you just need to know +;; what update-key does.) + +;; The next chapter shows what DI does when a component throws +;; halfway through start. From f98496f780fdad9d41d18e969a037cffb93c3615 Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Wed, 24 Jun 2026 16:18:31 +0400 Subject: [PATCH 02/11] Write Handling start failures chapter Rewrite the last tutorial chapter: rename "Graceful stop" to "Handling start failures", split the combined demo into two focused tests (built-deps-are-stopped, stop-errors-are-suppressed), and use inline ex-info throws instead of passing exceptions via the registry. Co-Authored-By: Claude Opus 4.7 (1M context) --- doc/cljdoc.edn | 2 +- .../di/tutorial/y_graceful_stop_test.clj | 90 ++++++++++++++----- 2 files changed, 67 insertions(+), 25 deletions(-) diff --git a/doc/cljdoc.edn b/doc/cljdoc.edn index 1e9d7791..3b757ed0 100644 --- a/doc/cljdoc.edn +++ b/doc/cljdoc.edn @@ -14,7 +14,7 @@ ["Wiring inside data" {:file "doc/tutorial/o_data_dsl_test.md"}] ["Transforming values" {:file "doc/tutorial/p_derive_test.md"}] ["Composition with update-key" {:file "doc/tutorial/x_update_key_test.md"}] - ["Graceful failures" {:file "doc/tutorial/y_graceful_stop_test.md"}] + ["Handling start failures" {:file "doc/tutorial/y_graceful_stop_test.md"}] ["Multimethods" {:file "doc/tutorial/r_multimethods_test.md"}]] ["Advanced" ["Tips" {:file "doc/how_to/tips_test.md"}] diff --git a/test/darkleaf/di/tutorial/y_graceful_stop_test.clj b/test/darkleaf/di/tutorial/y_graceful_stop_test.clj index b85fd8b2..a35f65cd 100644 --- a/test/darkleaf/di/tutorial/y_graceful_stop_test.clj +++ b/test/darkleaf/di/tutorial/y_graceful_stop_test.clj @@ -1,4 +1,4 @@ -;; # Graceful stop +;; # Handling start failures (ns darkleaf.di.tutorial.y-graceful-stop-test (:require @@ -6,32 +6,74 @@ [darkleaf.di.core :as di] [darkleaf.di.utils :refer [catch-some]])) -;; The DI tries to stop components that are already started -;; if another component fails while it is starting. +;; A real system can fail halfway through start: the database +;; came up, the queue worker came up, then the third component +;; threw. If nothing stops what was already built, you leak +;; resources — and some are non-shareable. A Jetty server holds +;; a port. If you cannot stop it, you cannot start another one +;; on the same port. The system reference is usually lost too, +;; so the only escape is to restart the REPL. -;; It throws the original exception. -;; All other possible exceptions are added as suppressed. +;; DI handles this for you. When a build fails, it stops what +;; was already built, and only then propagates the error. + +;; ## Built components are stopped before the error escapes + +;; `dep` builds successfully and returns the `stopped` atom. Its +;; `::di/stop` flips the atom to `true`. `root` depends on `dep` +;; and then throws. + +(defn dep + {::di/stop (fn [stopped] (reset! stopped true))} + [{stopped ::stopped}] + stopped) (defn root {::di/kind :component} - [{dep `dep - on-start-root-ex ::on-start-root-ex}] - (throw on-start-root-ex)) + [{_ `dep}] + (throw (ex-info "build failed" {}))) -(defn dep - {::di/stop (fn [on-stop-dep-ex] (throw on-stop-dep-ex))} - [{on-stop-dep-ex ::on-stop-dep-ex}] - on-stop-dep-ex) - - -(t/deftest graceful-start-test - (let [on-start-root-ex (ex-info "on start root" {}) - on-stop-dep-ex (ex-info "on stop dep" {}) - registry {::on-start-root-ex on-start-root-ex - ::on-stop-dep-ex on-stop-dep-ex} - ex (-> (di/start `root registry) - catch-some)] +;; `catch-some` captures whatever `di/start` throws. The original +;; failure is wrapped as `::di/build-failure`. `ex-cause` gives +;; you the original. `:stack` in `ex-data` shows the chain of +;; keys DI was building when it hit the gap. + +(t/deftest built-deps-are-stopped-test + (let [*stopped (atom false) + ex (catch-some (di/start `root {::stopped *stopped}))] + (t/is (= ::di/build-failure (-> ex ex-data :type))) + (t/is (= [`root] (-> ex ex-data :stack))) + (t/is (= "build failed" (-> ex ex-cause ex-message))) + ;; dep was stopped before the error propagated + (t/is @*stopped))) + +;; ## When a stop itself fails + +;; The exception that triggered the failure is what `di/start` +;; throws. But during the cleanup that follows, the stop +;; functions themselves can throw, and DI does not want you to +;; lose either kind of information. The original error stays as +;; the main exception (reachable via `ex-cause`). Each stop +;; failure is attached as a *suppressed* exception (the JVM +;; mechanism — see `Throwable/.addSuppressed` and +;; `Throwable/.getSuppressed`). + +(defn dep-stop-throws + {::di/stop (fn [_] (throw (ex-info "stop failed" {})))} + [] + :built) + +(defn root-after-bad-stop + {::di/kind :component} + [{_ `dep-stop-throws}] + (throw (ex-info "build failed" {}))) + +(t/deftest stop-errors-are-suppressed-test + (let [ex (catch-some (di/start `root-after-bad-stop))] (t/is (= ::di/build-failure (-> ex ex-data :type))) - (t/is (= [`root] (-> ex ex-data :stack))) - (t/is (= on-start-root-ex (-> ex ex-cause))) - (t/is (= [on-stop-dep-ex] (-> ex .getSuppressed seq))))) + (t/is (= "build failed" (-> ex ex-cause ex-message))) + (t/is (= ["stop failed"] (->> ex .getSuppressed (map ex-message)))))) + +;; That is the end of the tutorial. The +;; [Example app](/doc/example.md) shows the pieces working +;; together in a real project. From c006c37548dc873801967a04617c6bd070be0c85 Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Wed, 24 Jun 2026 16:25:25 +0400 Subject: [PATCH 03/11] Rename tutorial chapters to match new order File prefixes a..l now follow the v6 chapter order alphabetically: a-your-first-system, b-dependencies, c-stopping-components, d-interactive-development, e-registries, f-abstractions, g-environment-variables, h-starting-many-keys, i-wiring-inside-data, j-transforming-values, k-composition-with-update-key, l-handling-start-failures. Namespaces, internal cross-links, cljdoc.edn paths, and doc/example.md and doc/why_di.md links are all updated. doc/example.md REPL-redef link now points at "Interactive development" instead of "Intro" since that material moved chapters. Co-Authored-By: Claude Opus 4.7 (1M context) --- doc/cljdoc.edn | 20 +++++++++---------- doc/example.md | 2 +- doc/why_di.md | 2 +- ..._test.clj => a_your_first_system_test.clj} | 2 +- .../di/tutorial/b_dependencies_test.clj | 2 +- ...est.clj => c_stopping_components_test.clj} | 2 +- ...istries_test.clj => e_registries_test.clj} | 2 +- ...tions_test.clj => f_abstractions_test.clj} | 2 +- ...t.clj => g_environment_variables_test.clj} | 2 +- ...test.clj => h_starting_many_keys_test.clj} | 2 +- ...test.clj => i_wiring_inside_data_test.clj} | 2 +- ...est.clj => j_transforming_values_test.clj} | 4 ++-- ...=> k_composition_with_update_key_test.clj} | 2 +- ...clj => l_handling_start_failures_test.clj} | 2 +- 14 files changed, 24 insertions(+), 24 deletions(-) rename test/darkleaf/di/tutorial/{a_intro_test.clj => a_your_first_system_test.clj} (98%) rename test/darkleaf/di/tutorial/{c_stop_test.clj => c_stopping_components_test.clj} (96%) rename test/darkleaf/di/tutorial/{l_registries_test.clj => e_registries_test.clj} (98%) rename test/darkleaf/di/tutorial/{m_abstractions_test.clj => f_abstractions_test.clj} (98%) rename test/darkleaf/di/tutorial/{n_env_test.clj => g_environment_variables_test.clj} (98%) rename test/darkleaf/di/tutorial/{q_starting_many_keys_test.clj => h_starting_many_keys_test.clj} (96%) rename test/darkleaf/di/tutorial/{o_data_dsl_test.clj => i_wiring_inside_data_test.clj} (97%) rename test/darkleaf/di/tutorial/{p_derive_test.clj => j_transforming_values_test.clj} (93%) rename test/darkleaf/di/tutorial/{x_update_key_test.clj => k_composition_with_update_key_test.clj} (97%) rename test/darkleaf/di/tutorial/{y_graceful_stop_test.clj => l_handling_start_failures_test.clj} (98%) diff --git a/doc/cljdoc.edn b/doc/cljdoc.edn index 3b757ed0..f18b9e50 100644 --- a/doc/cljdoc.edn +++ b/doc/cljdoc.edn @@ -3,18 +3,18 @@ ["Changelog" {:file "CHANGELOG.md"}] ["Tutorial" ["Base" - ["Your first system" {:file "doc/tutorial/a_intro_test.md"}] + ["Your first system" {:file "doc/tutorial/a_your_first_system_test.md"}] ["Dependencies" {:file "doc/tutorial/b_dependencies_test.md"}] - ["Stopping components" {:file "doc/tutorial/c_stop_test.md"}] + ["Stopping components" {:file "doc/tutorial/c_stopping_components_test.md"}] ["Interactive development" {:file "doc/tutorial/d_interactive_development_test.md"}] - ["Registries" {:file "doc/tutorial/l_registries_test.md"}] - ["Abstractions" {:file "doc/tutorial/m_abstractions_test.md"}] - ["Environment variables" {:file "doc/tutorial/n_env_test.md"}] - ["Starting many keys" {:file "doc/tutorial/q_starting_many_keys_test.md"}] - ["Wiring inside data" {:file "doc/tutorial/o_data_dsl_test.md"}] - ["Transforming values" {:file "doc/tutorial/p_derive_test.md"}] - ["Composition with update-key" {:file "doc/tutorial/x_update_key_test.md"}] - ["Handling start failures" {:file "doc/tutorial/y_graceful_stop_test.md"}] + ["Registries" {:file "doc/tutorial/e_registries_test.md"}] + ["Abstractions" {:file "doc/tutorial/f_abstractions_test.md"}] + ["Environment variables" {:file "doc/tutorial/g_environment_variables_test.md"}] + ["Starting many keys" {:file "doc/tutorial/h_starting_many_keys_test.md"}] + ["Wiring inside data" {:file "doc/tutorial/i_wiring_inside_data_test.md"}] + ["Transforming values" {:file "doc/tutorial/j_transforming_values_test.md"}] + ["Composition with update-key" {:file "doc/tutorial/k_composition_with_update_key_test.md"}] + ["Handling start failures" {:file "doc/tutorial/l_handling_start_failures_test.md"}] ["Multimethods" {:file "doc/tutorial/r_multimethods_test.md"}]] ["Advanced" ["Tips" {:file "doc/how_to/tips_test.md"}] diff --git a/doc/example.md b/doc/example.md index 5ff6f61d..f46283af 100644 --- a/doc/example.md +++ b/doc/example.md @@ -53,4 +53,4 @@ user=> (stop) Redefine `example.core/root-handler` at the REPL and re-evaluate — the running system picks up the new implementation without a restart, as -described in the [Intro](/doc/tutorial/a_intro_test.md) chapter. +described in the [Interactive development](/doc/tutorial/d_interactive_development_test.md) chapter. diff --git a/doc/why_di.md b/doc/why_di.md index 11c638ca..d3e8dc27 100644 --- a/doc/why_di.md +++ b/doc/why_di.md @@ -135,5 +135,5 @@ lost. --- -The [tutorial](/doc/tutorial/a_intro_test.md) walks through each of +The [tutorial](/doc/tutorial/a_your_first_system_test.md) walks through each of these, one chapter at a time. diff --git a/test/darkleaf/di/tutorial/a_intro_test.clj b/test/darkleaf/di/tutorial/a_your_first_system_test.clj similarity index 98% rename from test/darkleaf/di/tutorial/a_intro_test.clj rename to test/darkleaf/di/tutorial/a_your_first_system_test.clj index 9a61b689..787ca16f 100644 --- a/test/darkleaf/di/tutorial/a_intro_test.clj +++ b/test/darkleaf/di/tutorial/a_your_first_system_test.clj @@ -1,6 +1,6 @@ ;; # Your first system -(ns darkleaf.di.tutorial.a-intro-test +(ns darkleaf.di.tutorial.a-your-first-system-test (:require [clojure.test :as t] [darkleaf.di.core :as di]) diff --git a/test/darkleaf/di/tutorial/b_dependencies_test.clj b/test/darkleaf/di/tutorial/b_dependencies_test.clj index 180a9bc9..f535d466 100644 --- a/test/darkleaf/di/tutorial/b_dependencies_test.clj +++ b/test/darkleaf/di/tutorial/b_dependencies_test.clj @@ -55,7 +55,7 @@ ;; would otherwise resolve from a var — a fake datasource in ;; tests, a different implementation in dev, and so on. ;; Registries are covered in detail in -;; [Registries](/doc/tutorial/l_registries_test.md). +;; [Registries](/doc/tutorial/e_registries_test.md). ;; ## Required by default diff --git a/test/darkleaf/di/tutorial/c_stop_test.clj b/test/darkleaf/di/tutorial/c_stopping_components_test.clj similarity index 96% rename from test/darkleaf/di/tutorial/c_stop_test.clj rename to test/darkleaf/di/tutorial/c_stopping_components_test.clj index e1b533db..6f063977 100644 --- a/test/darkleaf/di/tutorial/c_stop_test.clj +++ b/test/darkleaf/di/tutorial/c_stopping_components_test.clj @@ -1,6 +1,6 @@ ;; # Stopping components -(ns darkleaf.di.tutorial.c-stop-test +(ns darkleaf.di.tutorial.c-stopping-components-test (:require [clojure.test :as t] [darkleaf.di.core :as di])) diff --git a/test/darkleaf/di/tutorial/l_registries_test.clj b/test/darkleaf/di/tutorial/e_registries_test.clj similarity index 98% rename from test/darkleaf/di/tutorial/l_registries_test.clj rename to test/darkleaf/di/tutorial/e_registries_test.clj index b80c16b2..30bded53 100644 --- a/test/darkleaf/di/tutorial/l_registries_test.clj +++ b/test/darkleaf/di/tutorial/e_registries_test.clj @@ -1,6 +1,6 @@ ;; # Registries -(ns darkleaf.di.tutorial.l-registries-test +(ns darkleaf.di.tutorial.e-registries-test (:require [clojure.test :as t] [darkleaf.di.core :as di])) diff --git a/test/darkleaf/di/tutorial/m_abstractions_test.clj b/test/darkleaf/di/tutorial/f_abstractions_test.clj similarity index 98% rename from test/darkleaf/di/tutorial/m_abstractions_test.clj rename to test/darkleaf/di/tutorial/f_abstractions_test.clj index 6d548bf1..167ca269 100644 --- a/test/darkleaf/di/tutorial/m_abstractions_test.clj +++ b/test/darkleaf/di/tutorial/f_abstractions_test.clj @@ -1,6 +1,6 @@ ;; # Abstractions -(ns darkleaf.di.tutorial.m-abstractions-test +(ns darkleaf.di.tutorial.f-abstractions-test (:require [clojure.test :as t] [darkleaf.di.core :as di] diff --git a/test/darkleaf/di/tutorial/n_env_test.clj b/test/darkleaf/di/tutorial/g_environment_variables_test.clj similarity index 98% rename from test/darkleaf/di/tutorial/n_env_test.clj rename to test/darkleaf/di/tutorial/g_environment_variables_test.clj index d412d37b..a0feb888 100644 --- a/test/darkleaf/di/tutorial/n_env_test.clj +++ b/test/darkleaf/di/tutorial/g_environment_variables_test.clj @@ -1,6 +1,6 @@ ;; # Environment variables -(ns darkleaf.di.tutorial.n-env-test +(ns darkleaf.di.tutorial.g-environment-variables-test (:require [clojure.test :as t] [darkleaf.di.core :as di])) diff --git a/test/darkleaf/di/tutorial/q_starting_many_keys_test.clj b/test/darkleaf/di/tutorial/h_starting_many_keys_test.clj similarity index 96% rename from test/darkleaf/di/tutorial/q_starting_many_keys_test.clj rename to test/darkleaf/di/tutorial/h_starting_many_keys_test.clj index c375f386..10f73362 100644 --- a/test/darkleaf/di/tutorial/q_starting_many_keys_test.clj +++ b/test/darkleaf/di/tutorial/h_starting_many_keys_test.clj @@ -1,6 +1,6 @@ ;; # Starting many keys -(ns darkleaf.di.tutorial.q-starting-many-keys-test +(ns darkleaf.di.tutorial.h-starting-many-keys-test (:require [darkleaf.di.core :as di] [clojure.test :as t])) diff --git a/test/darkleaf/di/tutorial/o_data_dsl_test.clj b/test/darkleaf/di/tutorial/i_wiring_inside_data_test.clj similarity index 97% rename from test/darkleaf/di/tutorial/o_data_dsl_test.clj rename to test/darkleaf/di/tutorial/i_wiring_inside_data_test.clj index cafd27e9..7d2891c6 100644 --- a/test/darkleaf/di/tutorial/o_data_dsl_test.clj +++ b/test/darkleaf/di/tutorial/i_wiring_inside_data_test.clj @@ -1,6 +1,6 @@ ;; # Wiring inside data -(ns darkleaf.di.tutorial.o-data-dsl-test +(ns darkleaf.di.tutorial.i-wiring-inside-data-test (:require [clojure.test :as t] [darkleaf.di.core :as di])) diff --git a/test/darkleaf/di/tutorial/p_derive_test.clj b/test/darkleaf/di/tutorial/j_transforming_values_test.clj similarity index 93% rename from test/darkleaf/di/tutorial/p_derive_test.clj rename to test/darkleaf/di/tutorial/j_transforming_values_test.clj index 23dfebdf..e99cd894 100644 --- a/test/darkleaf/di/tutorial/p_derive_test.clj +++ b/test/darkleaf/di/tutorial/j_transforming_values_test.clj @@ -1,6 +1,6 @@ ;; # Transforming values -(ns darkleaf.di.tutorial.p-derive-test +(ns darkleaf.di.tutorial.j-transforming-values-test (:require [clojure.test :as t] [darkleaf.di.core :as di])) @@ -38,7 +38,7 @@ (t/is (= 8080 @root)))) ;; Use whichever reads better. For env vars specifically, -;; [`di/env-parsing`](/doc/tutorial/n_env_test.md) is usually +;; [`di/env-parsing`](/doc/tutorial/g_environment_variables_test.md) is usually ;; nicer — it registers a parser once for a whole keyword ;; namespace (`:env.long`, `:env.json`). `di/derive` is the ;; general tool, useful when the transformation does not fit the diff --git a/test/darkleaf/di/tutorial/x_update_key_test.clj b/test/darkleaf/di/tutorial/k_composition_with_update_key_test.clj similarity index 97% rename from test/darkleaf/di/tutorial/x_update_key_test.clj rename to test/darkleaf/di/tutorial/k_composition_with_update_key_test.clj index d75acd9e..af4f2472 100644 --- a/test/darkleaf/di/tutorial/x_update_key_test.clj +++ b/test/darkleaf/di/tutorial/k_composition_with_update_key_test.clj @@ -1,6 +1,6 @@ ;; # Composition with `update-key` -(ns darkleaf.di.tutorial.x-update-key-test +(ns darkleaf.di.tutorial.k-composition-with-update-key-test (:require [clojure.test :as t] [darkleaf.di.core :as di])) diff --git a/test/darkleaf/di/tutorial/y_graceful_stop_test.clj b/test/darkleaf/di/tutorial/l_handling_start_failures_test.clj similarity index 98% rename from test/darkleaf/di/tutorial/y_graceful_stop_test.clj rename to test/darkleaf/di/tutorial/l_handling_start_failures_test.clj index a35f65cd..4b2e7fe9 100644 --- a/test/darkleaf/di/tutorial/y_graceful_stop_test.clj +++ b/test/darkleaf/di/tutorial/l_handling_start_failures_test.clj @@ -1,6 +1,6 @@ ;; # Handling start failures -(ns darkleaf.di.tutorial.y-graceful-stop-test +(ns darkleaf.di.tutorial.l-handling-start-failures-test (:require [clojure.test :as t] [darkleaf.di.core :as di] From 6c6375b8b7d3faa4c453ea658c3cb0200db615ed Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Wed, 24 Jun 2026 16:33:01 +0400 Subject: [PATCH 04/11] Move Multimethods to How-to and tighten the prose defmulti-as-service is a recipe, not a tutorial step. Moved the file to test/darkleaf/di/how_to/ with the matching namespace, and trimmed the intro and optional-dep prose. Co-Authored-By: Claude Opus 4.7 (1M context) --- doc/cljdoc.edn | 4 ++-- .../multimethods_test.clj} | 16 +++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) rename test/darkleaf/di/{tutorial/r_multimethods_test.clj => how_to/multimethods_test.clj} (63%) diff --git a/doc/cljdoc.edn b/doc/cljdoc.edn index f18b9e50..c1380842 100644 --- a/doc/cljdoc.edn +++ b/doc/cljdoc.edn @@ -14,13 +14,13 @@ ["Wiring inside data" {:file "doc/tutorial/i_wiring_inside_data_test.md"}] ["Transforming values" {:file "doc/tutorial/j_transforming_values_test.md"}] ["Composition with update-key" {:file "doc/tutorial/k_composition_with_update_key_test.md"}] - ["Handling start failures" {:file "doc/tutorial/l_handling_start_failures_test.md"}] - ["Multimethods" {:file "doc/tutorial/r_multimethods_test.md"}]] + ["Handling start failures" {:file "doc/tutorial/l_handling_start_failures_test.md"}]] ["Advanced" ["Tips" {:file "doc/how_to/tips_test.md"}] ["Add a side dependency" {:file "doc/tutorial/x_add_side_dependency_test.md"}] ["Log" {:file "doc/tutorial/x_log_test.md"}] ["Inspect" {:file "doc/tutorial/x_inspect_test.md"}] + ["Multimethods" {:file "doc/how_to/multimethods_test.md"}] ["Multi-arity services" {:file "doc/how_to/multi_arity_service_test.md"}] ["Multi system" {:file "doc/tutorial/z_multi_system_test.md"}] ["Two Databases" {:file "doc/tutorial/z_two_databases_test.md"}]]] diff --git a/test/darkleaf/di/tutorial/r_multimethods_test.clj b/test/darkleaf/di/how_to/multimethods_test.clj similarity index 63% rename from test/darkleaf/di/tutorial/r_multimethods_test.clj rename to test/darkleaf/di/how_to/multimethods_test.clj index 2b09db0a..12878c41 100644 --- a/test/darkleaf/di/tutorial/r_multimethods_test.clj +++ b/test/darkleaf/di/how_to/multimethods_test.clj @@ -1,13 +1,13 @@ ;; # Multimethods -(ns darkleaf.di.tutorial.r-multimethods-test +(ns darkleaf.di.how-to.multimethods-test (:require [clojure.test :as t] [darkleaf.di.core :as di])) -;; You can use `defmulti` like `defn` to define a service. -;; Unlike `defn`, there is no way to get a definition of dependencies -;; and we have to define them as `::di/deps` on metadata. +;; A `defmulti` can be a service, but DI cannot read its argument +;; list the way it reads a `defn`. Declare the dependencies in +;; metadata under `::di/deps`. (defmulti service {::di/deps [::x]} @@ -20,9 +20,11 @@ (with-open [root (di/start `service {::x :value})] (t/is (= [:kind :value] (root :kind))))) -;; `::di/deps` defines only required dependencies, mostly for simplicity. -;; If you need to use an optional dependency, -;; simply convert it to a required dependency by adding a default value. +;; ## Optional dependencies + +;; `::di/deps` only declares required dependencies. To make a +;; dependency optional, wrap it with `di/derive` and supply a +;; fallback: (defn- wrap-default [x default] (if (some? x) x default)) From d65e9dec26ffb270278a2a9ccd28b726daad1764 Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Wed, 24 Jun 2026 16:45:27 +0400 Subject: [PATCH 05/11] Move Side dependencies to How-to and expand the rationale Rename "Add a side dependency" to "Side dependencies" and move the file to test/darkleaf/di/how_to/. Reframe the recipe: tighten the hook to migrations-as-the-canonical-case and add a "Why not just list it as another root?" section that contrasts add-side-dependency with the vector-form di/start, pointing at the subsystem-registry pattern. Co-Authored-By: Claude Opus 4.7 (1M context) --- doc/cljdoc.edn | 2 +- .../di/how_to/side_dependencies_test.clj | 68 +++++++++++++++++++ .../tutorial/x_add_side_dependency_test.clj | 30 -------- 3 files changed, 69 insertions(+), 31 deletions(-) create mode 100644 test/darkleaf/di/how_to/side_dependencies_test.clj delete mode 100644 test/darkleaf/di/tutorial/x_add_side_dependency_test.clj diff --git a/doc/cljdoc.edn b/doc/cljdoc.edn index c1380842..bb16ebe4 100644 --- a/doc/cljdoc.edn +++ b/doc/cljdoc.edn @@ -17,7 +17,7 @@ ["Handling start failures" {:file "doc/tutorial/l_handling_start_failures_test.md"}]] ["Advanced" ["Tips" {:file "doc/how_to/tips_test.md"}] - ["Add a side dependency" {:file "doc/tutorial/x_add_side_dependency_test.md"}] + ["Side dependencies" {:file "doc/how_to/side_dependencies_test.md"}] ["Log" {:file "doc/tutorial/x_log_test.md"}] ["Inspect" {:file "doc/tutorial/x_inspect_test.md"}] ["Multimethods" {:file "doc/how_to/multimethods_test.md"}] diff --git a/test/darkleaf/di/how_to/side_dependencies_test.clj b/test/darkleaf/di/how_to/side_dependencies_test.clj new file mode 100644 index 00000000..7b8e365c --- /dev/null +++ b/test/darkleaf/di/how_to/side_dependencies_test.clj @@ -0,0 +1,68 @@ +;; # Side dependencies + +(ns darkleaf.di.how-to.side-dependencies-test + (:require + [clojure.test :as t] + [darkleaf.di.core :as di])) + +;; Some components must run at system start, but nothing else +;; references them — database migrations are the classic case. +;; `di/add-side-dependency` pulls such a component into the +;; system without forcing the root to declare it as a dependency. + +(defn root + {::di/kind :component} + [] + 'root) + +(defn migrations + {::di/kind :component} + [{::keys [*migrated?]}] + (reset! *migrated? true)) + +(t/deftest add-side-dependency-test + (let [*migrated? (atom false)] + (with-open [root (di/start `root + (di/add-side-dependency `migrations) + {::*migrated? *migrated?})] + ;; `migrations` ran as part of start... + (t/is @*migrated?) + ;; ...even though `root` does not reference it. + (t/is (= 'root @root))))) + +;; ## Why not just list it as another root? + +;; You could — `di/start` takes a vector of keys and builds them +;; in the order you list them. Migrations must come first so +;; they run before the app: + +;; ```clojure +;; (di/start [`migrations `root] ...) +;; ``` + +;; But then the start call has to enumerate every cross-cutting +;; concern in the right order — migrations before the app, and +;; so on. `add-side-dependency` lets each subsystem declare its +;; setup inside its own registry, so the top-level start stays +;; clean. + +;; The usual pattern: applications are split into subsystems, and +;; each subsystem ships its own `registry` function that +;; contributes components and middlewares. A subsystem that owns +;; migrations declares its side dependency inside its own +;; registry. The root never mentions it. + +;; ```clojure +;; ;; users subsystem +;; (defn registry [_] +;; [(di/add-side-dependency `migrations)]) +;; +;; ;; main system composes subsystems +;; (di/start `app +;; (users/registry flags) +;; (orders/registry flags)) +;; ``` + +;; See +;; [Composition with `update-key`](/doc/tutorial/k_composition_with_update_key_test.md) +;; for the broader pattern. diff --git a/test/darkleaf/di/tutorial/x_add_side_dependency_test.clj b/test/darkleaf/di/tutorial/x_add_side_dependency_test.clj deleted file mode 100644 index cf943ef1..00000000 --- a/test/darkleaf/di/tutorial/x_add_side_dependency_test.clj +++ /dev/null @@ -1,30 +0,0 @@ -;; # Add side dependency - -(ns darkleaf.di.tutorial.x-add-side-dependency-test - (:require - [clojure.test :as t] - [darkleaf.di.core :as di])) - -;; Actually, the rest of the `di/start` arguments are middlewares. -;; Maps and collections are special cases of ones. -;; Middlewares allow us to implement extra features. - -;; In this test I'll show you how to perform a side effect like a database migration. - -(defn root - {::di/kind :component} - [] - 'root) - -(defn migrations - {::di/kind :component} - [{::keys [*migrated?]}] - (reset! *migrated? true)) - -(t/deftest add-side-dependency-test - (let [*migrated? (atom false)] - (with-open [root (di/start `root - (di/add-side-dependency `migrations) - {::*migrated? *migrated?})] - (t/is @*migrated?) - (t/is (= 'root @root))))) From e72c68fd16ab08b019ec7663ce7017acfbbc0393 Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Wed, 24 Jun 2026 16:55:57 +0400 Subject: [PATCH 06/11] Move Log to How-to and simplify the prose Rename "Log" to "Logging system lifecycle" and move the file to test/darkleaf/di/how_to/. Drop the "middleware" word in favour of plainer phrasing about argument order, and add a short note about why the test compares pr-str values. Co-Authored-By: Claude Opus 4.7 (1M context) --- doc/cljdoc.edn | 2 +- .../x_log_test.clj => how_to/log_test.clj} | 30 +++++++++++-------- 2 files changed, 18 insertions(+), 14 deletions(-) rename test/darkleaf/di/{tutorial/x_log_test.clj => how_to/log_test.clj} (60%) diff --git a/doc/cljdoc.edn b/doc/cljdoc.edn index bb16ebe4..e5f6a274 100644 --- a/doc/cljdoc.edn +++ b/doc/cljdoc.edn @@ -18,7 +18,7 @@ ["Advanced" ["Tips" {:file "doc/how_to/tips_test.md"}] ["Side dependencies" {:file "doc/how_to/side_dependencies_test.md"}] - ["Log" {:file "doc/tutorial/x_log_test.md"}] + ["Logging system lifecycle" {:file "doc/how_to/log_test.md"}] ["Inspect" {:file "doc/tutorial/x_inspect_test.md"}] ["Multimethods" {:file "doc/how_to/multimethods_test.md"}] ["Multi-arity services" {:file "doc/how_to/multi_arity_service_test.md"}] diff --git a/test/darkleaf/di/tutorial/x_log_test.clj b/test/darkleaf/di/how_to/log_test.clj similarity index 60% rename from test/darkleaf/di/tutorial/x_log_test.clj rename to test/darkleaf/di/how_to/log_test.clj index 6348c9e4..5a073fd1 100644 --- a/test/darkleaf/di/tutorial/x_log_test.clj +++ b/test/darkleaf/di/how_to/log_test.clj @@ -1,14 +1,14 @@ -;; # Log +;; # Logging system lifecycle -(ns darkleaf.di.tutorial.x-log-test +(ns darkleaf.di.how-to.log-test (:require [clojure.test :as t] [darkleaf.di.core :as di])) -;; `di/log` is a middleware that fires a callback every time a -;; factory is built and every time it is demolished. Each callback -;; receives `{:keys [key object]}` — the key and the built value (or -;; the value about to be demolished). +;; `di/log` fires a callback every time a factory is built and +;; every time it is demolished. Each callback receives +;; `{:keys [key object]}` — the key and the built value (or the +;; value about to be demolished). ;; Reach for it to instrument the system at runtime — time each ;; build or demolish step, or stream lifecycle events into your @@ -16,13 +16,13 @@ ;; actually running the system, use ;; [`di/inspect`](/doc/tutorial/x_inspect_test.md) instead. -;; `di/log` must be the last middleware in the chain — it wraps every -;; factory the registry exposes, and anything appended after it ends -;; up between `log` and the original factory. +;; Put `di/log` last when you call `di/start`. `log` reports +;; every factory before it in the argument list. Anything after +;; `log` is not reported. ;; The components below form a chain `c → b → a`. Builds run in -;; dependency order; demolitions run in reverse — last built, first -;; demolished. Note also how the printed forms differ: a component +;; dependency order. Demolitions run in reverse — last built, +;; first demolished. The printed forms also differ: a component ;; shows its built value, a service shows the var it points to. (defn a @@ -38,6 +38,10 @@ [{b `b}] :c) +;; The callbacks log via `pr-str` because service `b` builds to +;; a partial fn — not `=`-comparable to a literal, but it has a +;; custom print method that yields a stable string. + (t/deftest log-test (let [logs (atom []) after-build! (fn [{:keys [key object]}] @@ -50,10 +54,10 @@ (di/stop root) (t/is (= [[:built `a ":a"] [:built `b - "#darkleaf.di.core/service #'darkleaf.di.tutorial.x-log-test/b"] + "#darkleaf.di.core/service #'darkleaf.di.how-to.log-test/b"] [:built `c ":c"] [:demolished `c ":c"] [:demolished `b - "#darkleaf.di.core/service #'darkleaf.di.tutorial.x-log-test/b"] + "#darkleaf.di.core/service #'darkleaf.di.how-to.log-test/b"] [:demolished `a ":a"]] @logs)))) From 9c1844259004a559824fc3f18040beb7bc2bf2d0 Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Wed, 24 Jun 2026 17:08:29 +0400 Subject: [PATCH 07/11] Move ns-publics to How-to and add it to the TOC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename the chapter to "All public vars as a component" and move the file to test/darkleaf/di/how_to/. Cascade the namespace change through x_inspect_test.clj (which imports it) and the ns-publics docstring in core.clj. Drop the "middleware" word in the prose. Also add the chapter to cljdoc.edn — it was missing from the TOC since long before this restructure. Co-Authored-By: Claude Opus 4.7 (1M context) --- doc/cljdoc.edn | 3 +- src/darkleaf/di/core.clj | 2 +- test/darkleaf/di/how_to/ns_publics_test.clj | 47 +++++++++++++++++++ test/darkleaf/di/tutorial/x_inspect_test.clj | 8 ++-- .../di/tutorial/x_ns_publics_test.clj | 46 ------------------ 5 files changed, 54 insertions(+), 52 deletions(-) create mode 100644 test/darkleaf/di/how_to/ns_publics_test.clj delete mode 100644 test/darkleaf/di/tutorial/x_ns_publics_test.clj diff --git a/doc/cljdoc.edn b/doc/cljdoc.edn index e5f6a274..70691b0c 100644 --- a/doc/cljdoc.edn +++ b/doc/cljdoc.edn @@ -20,7 +20,8 @@ ["Side dependencies" {:file "doc/how_to/side_dependencies_test.md"}] ["Logging system lifecycle" {:file "doc/how_to/log_test.md"}] ["Inspect" {:file "doc/tutorial/x_inspect_test.md"}] - ["Multimethods" {:file "doc/how_to/multimethods_test.md"}] + ["Multimethods" {:file "doc/how_to/multimethods_test.md"}] + ["All public vars as a component" {:file "doc/how_to/ns_publics_test.md"}] ["Multi-arity services" {:file "doc/how_to/multi_arity_service_test.md"}] ["Multi system" {:file "doc/tutorial/z_multi_system_test.md"}] ["Two Databases" {:file "doc/tutorial/z_two_databases_test.md"}]]] diff --git a/src/darkleaf/di/core.clj b/src/darkleaf/di/core.clj index 70ac94d5..55c6873f 100644 --- a/src/darkleaf/di/core.clj +++ b/src/darkleaf/di/core.clj @@ -838,7 +838,7 @@ This enables access to all public components, which is useful for testing. - See the test `darkleaf.di.tutorial.x-ns-publics-test`. + See the test `darkleaf.di.how-to.ns-publics-test`. ```clojure (di/start :ns-publics/io.github.my.ns (di/ns-publics)) diff --git a/test/darkleaf/di/how_to/ns_publics_test.clj b/test/darkleaf/di/how_to/ns_publics_test.clj new file mode 100644 index 00000000..f8cbd9e7 --- /dev/null +++ b/test/darkleaf/di/how_to/ns_publics_test.clj @@ -0,0 +1,47 @@ +;; # All public vars as a component + +(ns darkleaf.di.how-to.ns-publics-test + (:require + [clojure.test :as t] + [darkleaf.di.core :as di])) + +;; `di/ns-publics` treats every public var of a namespace as a +;; component or service and bundles them under +;; `:ns-publics/`. Starting that key gives you a map of +;; var name → built object — handy when you want a group of +;; related components and services without listing each one +;; explicitly. + +;; Vars holding `nil` or unbound vars are skipped. + +(def nil-component nil) ; excluded + +(def unbound-component) ; excluded + +(defn component + {::di/kind :component} + [] + :component) + +(defn service [{component `component} arg] + [component arg]) + +(t/deftest ok-test + (with-open [system (di/start :ns-publics/darkleaf.di.how-to.ns-publics-test + (di/ns-publics))] + (t/is (map? @system)) + (t/is (= #{:component :service :ok-test} + (set (keys @system)))) + (t/is (= :component (:component system))) + (t/is (= [:component :my-arg] ((:service system) :my-arg))))) + +;; ## In practice + +;; The feature originated in test infrastructure: a single global +;; test system was kept running, with adapter namespaces (a +;; database client, etc.) registered as roots via `ns-publics` so +;; any test could reach into them directly. `di/->memoize` later +;; covered the same ground with less ceremony — each test starts +;; just the keys it touches against a shared cache. `ns-publics` +;; still works as documented, but `->memoize` is the preferred +;; way to do that now. diff --git a/test/darkleaf/di/tutorial/x_inspect_test.clj b/test/darkleaf/di/tutorial/x_inspect_test.clj index 611778e8..d8caeeaf 100644 --- a/test/darkleaf/di/tutorial/x_inspect_test.clj +++ b/test/darkleaf/di/tutorial/x_inspect_test.clj @@ -5,7 +5,7 @@ [clojure.test :as t] [darkleaf.di.core :as di] [darkleaf.di.protocols :as p] - [darkleaf.di.tutorial.x-ns-publics-test :as x-ns-publics-test])) + [darkleaf.di.how-to.ns-publics-test :as x-ns-publics-test])) ;; `di/inspect` takes the same arguments as `di/start` but builds ;; nothing. It walks the registry and returns a vector describing @@ -290,13 +290,13 @@ ;; `:middleware` factories standing in front of the keys they expose. (t/deftest ns-publics-test - (t/is (= [{:key :ns-publics/darkleaf.di.tutorial.x-ns-publics-test + (t/is (= [{:key :ns-publics/darkleaf.di.how-to.ns-publics-test :dependencies {`x-ns-publics-test/service :required `x-ns-publics-test/component :required `x-ns-publics-test/ok-test :required} :description {::di/kind :middleware :middleware ::di/ns-publics - :ns 'darkleaf.di.tutorial.x-ns-publics-test + :ns 'darkleaf.di.how-to.ns-publics-test ::di/root true}} {:key `x-ns-publics-test/service :dependencies {`x-ns-publics-test/component :required} @@ -309,7 +309,7 @@ :description {::di/kind :trivial :object x-ns-publics-test/ok-test ::di/variable #'x-ns-publics-test/ok-test}}] - (di/inspect :ns-publics/darkleaf.di.tutorial.x-ns-publics-test + (di/inspect :ns-publics/darkleaf.di.how-to.ns-publics-test (di/ns-publics))))) diff --git a/test/darkleaf/di/tutorial/x_ns_publics_test.clj b/test/darkleaf/di/tutorial/x_ns_publics_test.clj deleted file mode 100644 index 9ba03f48..00000000 --- a/test/darkleaf/di/tutorial/x_ns_publics_test.clj +++ /dev/null @@ -1,46 +0,0 @@ -;; # Ns publics - -(ns darkleaf.di.tutorial.x-ns-publics-test - (:require - [clojure.test :as t] - [darkleaf.di.core :as di])) - -;; `di/ns-publics` is a middleware that treats every public var of a -;; namespace as a component or service to be built, bundling them -;; under `:ns-publics/`. Starting that key gives you a map -;; of var name → built object — handy when you want a group of -;; related components and services without listing each one -;; explicitly. - -;; Vars holding `nil` or unbound vars are skipped. - -(def nil-component nil) ; excluded - -(def unbound-component) ; excluded - -(defn component - {::di/kind :component} - [] - :component) - -(defn service [{component `component} arg] - [component arg]) - -(t/deftest ok-test - (with-open [system (di/start :ns-publics/darkleaf.di.tutorial.x-ns-publics-test - (di/ns-publics))] - (t/is (map? @system)) - (t/is (= #{:component :service :ok-test} - (set (keys @system)))) - (t/is (= :component (:component system))) - (t/is (= [:component :my-arg] ((:service system) :my-arg))))) - -;; ## In practice - -;; This middleware originated in test infrastructure: a single global -;; test system was kept running, with adapter namespaces (a database -;; client, etc.) registered as roots via `ns-publics` so any test -;; could reach into them directly. `di/->memoize` later covered the -;; same ground with less ceremony — each test starts just the keys -;; it touches against a shared cache. `ns-publics` still works as -;; documented, but `->memoize` is the preferred way to do that now. From c943388b20d53fc67bb063bdfb7a84fde09b127b Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Wed, 24 Jun 2026 17:16:18 +0400 Subject: [PATCH 08/11] Move Multi-system to How-to and add a concrete use case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename "Multi system" to "Multiple systems" and move the file to test/darkleaf/di/how_to/. Replace the abstract intro with a concrete scenario — several web apps sharing the same database pool — and call out the identical-instance assertion inline. Co-Authored-By: Claude Opus 4.7 (1M context) --- doc/cljdoc.edn | 2 +- .../multiple_systems_test.clj} | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) rename test/darkleaf/di/{tutorial/z_multi_system_test.clj => how_to/multiple_systems_test.clj} (57%) diff --git a/doc/cljdoc.edn b/doc/cljdoc.edn index 70691b0c..97367ab1 100644 --- a/doc/cljdoc.edn +++ b/doc/cljdoc.edn @@ -23,7 +23,7 @@ ["Multimethods" {:file "doc/how_to/multimethods_test.md"}] ["All public vars as a component" {:file "doc/how_to/ns_publics_test.md"}] ["Multi-arity services" {:file "doc/how_to/multi_arity_service_test.md"}] - ["Multi system" {:file "doc/tutorial/z_multi_system_test.md"}] + ["Multiple systems" {:file "doc/how_to/multiple_systems_test.md"}] ["Two Databases" {:file "doc/tutorial/z_two_databases_test.md"}]]] ["Reference" ["Middleware types" {:file "doc/reference/middleware_types_test.md"}]] diff --git a/test/darkleaf/di/tutorial/z_multi_system_test.clj b/test/darkleaf/di/how_to/multiple_systems_test.clj similarity index 57% rename from test/darkleaf/di/tutorial/z_multi_system_test.clj rename to test/darkleaf/di/how_to/multiple_systems_test.clj index 6f367821..3ecb1cb2 100644 --- a/test/darkleaf/di/tutorial/z_multi_system_test.clj +++ b/test/darkleaf/di/how_to/multiple_systems_test.clj @@ -1,14 +1,16 @@ -;; # Multi system -(ns darkleaf.di.tutorial.z-multi-system-test +;; # Multiple systems + +(ns darkleaf.di.how-to.multiple-systems-test (:require [clojure.test :as t] [darkleaf.di.core :as di])) -;; In some cases, you may need multiple systems and to share a subsystem between them. -;; In that case, just pass the subsystem in the registry. - -;; To get a value of the subsystem, you should `deref` it as you would for a regular system root. -;; Also you should manually stop systems in reverse order. +;; Sometimes you need several running systems that share a +;; subsystem — for example a few independent web apps backed by +;; the same database pool. Start the shared subsystem first, +;; deref it for its built value, and pass that value through the +;; registry of each downstream system. Stop them in reverse +;; order when you are done. (defn shared {::di/kind :component} @@ -29,4 +31,5 @@ ::name :b})] (t/is (= :a (first @a))) (t/is (= :b (first @b))) + ;; both servers see the same shared instance (t/is (identical? (second @a) (second @b))))) From 7bdc5c3d488e62744859301ab2d91cad28ab3096 Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Wed, 24 Jun 2026 17:24:02 +0400 Subject: [PATCH 09/11] Convert Middleware types reference to plain markdown A Reference page is descriptive prose with example snippets, not a literate test file. Move the stub to a tracked plain markdown under doc/reference/middleware_types.md, drop the .clj wrapper, drop /doc/reference/ from .gitignore so the page is committed directly, and update the forward-links in the Registries, Environment variables, and Composition tutorial chapters. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 1 - doc/cljdoc.edn | 2 +- doc/reference/middleware_types.md | 6 ++++++ test/darkleaf/di/reference/middleware_types_test.clj | 11 ----------- test/darkleaf/di/tutorial/e_registries_test.clj | 2 +- .../di/tutorial/g_environment_variables_test.clj | 2 +- .../tutorial/k_composition_with_update_key_test.clj | 2 +- 7 files changed, 10 insertions(+), 16 deletions(-) create mode 100644 doc/reference/middleware_types.md delete mode 100644 test/darkleaf/di/reference/middleware_types_test.clj diff --git a/.gitignore b/.gitignore index 9a5164b0..1c72e531 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ /pom.xml /doc/tutorial/ /doc/how_to/ -/doc/reference/ #/doc/_*.md .clj-kondo/ .lsp/ \ No newline at end of file diff --git a/doc/cljdoc.edn b/doc/cljdoc.edn index 97367ab1..3668f481 100644 --- a/doc/cljdoc.edn +++ b/doc/cljdoc.edn @@ -26,6 +26,6 @@ ["Multiple systems" {:file "doc/how_to/multiple_systems_test.md"}] ["Two Databases" {:file "doc/tutorial/z_two_databases_test.md"}]]] ["Reference" - ["Middleware types" {:file "doc/reference/middleware_types_test.md"}]] + ["Middleware types" {:file "doc/reference/middleware_types.md"}]] ["Example app" {:file "doc/example.md"}] ["Integrant vs DI" {:file "doc/integrant.md"}]]} diff --git a/doc/reference/middleware_types.md b/doc/reference/middleware_types.md new file mode 100644 index 00000000..29f1987c --- /dev/null +++ b/doc/reference/middleware_types.md @@ -0,0 +1,6 @@ +# Middleware types + +TODO: describe the registry-middleware abstraction +(`registry -> key -> Factory`) and list the built-in middlewares: +`di/update-key`, `di/env-parsing`, `di/add-side-dependency`, +`di/log`, `di/ns-publics`, the map form, and the seqable form. diff --git a/test/darkleaf/di/reference/middleware_types_test.clj b/test/darkleaf/di/reference/middleware_types_test.clj deleted file mode 100644 index 206ac60a..00000000 --- a/test/darkleaf/di/reference/middleware_types_test.clj +++ /dev/null @@ -1,11 +0,0 @@ -;; # Middleware types - -(ns darkleaf.di.reference.middleware-types-test - (:require - [clojure.test :as t])) - -;; TODO: describe the registry-middleware abstraction -;; (`registry -> key -> Factory`) and list the built-in -;; middlewares: `di/update-key`, `di/env-parsing`, -;; `di/add-side-dependency`, `di/log`, `di/ns-publics`, -;; map form, seqable form. diff --git a/test/darkleaf/di/tutorial/e_registries_test.clj b/test/darkleaf/di/tutorial/e_registries_test.clj index 30bded53..90b57927 100644 --- a/test/darkleaf/di/tutorial/e_registries_test.clj +++ b/test/darkleaf/di/tutorial/e_registries_test.clj @@ -63,7 +63,7 @@ (t/is (= [:value :a :b] @root)))) ;; The map form is one of several registry shapes — see -;; [Middleware types](/doc/reference/middleware_types_test.md) +;; [Middleware types](/doc/reference/middleware_types.md) ;; for the full picture. The tutorial only needs the map form. ;; The next chapter introduces keyword keys — a way to decouple diff --git a/test/darkleaf/di/tutorial/g_environment_variables_test.clj b/test/darkleaf/di/tutorial/g_environment_variables_test.clj index a0feb888..6670f996 100644 --- a/test/darkleaf/di/tutorial/g_environment_variables_test.clj +++ b/test/darkleaf/di/tutorial/g_environment_variables_test.clj @@ -61,7 +61,7 @@ ;; anything. ;; (`di/env-parsing` is one of several registry shapes — see -;; [Middleware types](/doc/reference/middleware_types_test.md) +;; [Middleware types](/doc/reference/middleware_types.md) ;; for the wider picture.) ;; ## Required vs optional diff --git a/test/darkleaf/di/tutorial/k_composition_with_update_key_test.clj b/test/darkleaf/di/tutorial/k_composition_with_update_key_test.clj index af4f2472..3b7cc31d 100644 --- a/test/darkleaf/di/tutorial/k_composition_with_update_key_test.clj +++ b/test/darkleaf/di/tutorial/k_composition_with_update_key_test.clj @@ -74,7 +74,7 @@ [path (handler :req)]))))) ;; (Under the hood `di/update-key` is a registry middleware — see -;; [Middleware types](/doc/reference/middleware_types_test.md) +;; [Middleware types](/doc/reference/middleware_types.md) ;; for what that means. For everyday use you just need to know ;; what update-key does.) From 07e7b453b95f8c3360101454e7b6289f3ca56c58 Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Wed, 24 Jun 2026 17:44:03 +0400 Subject: [PATCH 10/11] Update CLAUDE.md with conventions from the v6 docs restructure Bring the project notes in line with how the docs are organised now: - Mention doc/how_to/ (generated like tutorial/) and doc/reference/ (plain tracked markdown), plus the _journey.md / _structure.md planning artifacts. - Update the article cross-link example from the old a_intro_test path to the renamed a_your_first_system_test, and document the full cljdoc URL format for article-to-API-var links. - Add a new "Documentation conventions" section: audience, voice, terminology (no "middleware" in tutorial/how-to, math-style names, keyword-vs-symbol intent), directory layout, test idioms in chapter files (vs. strict regular tests), and when a Reference page earns its keep. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 102 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 270817c9..2feef99f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,9 +15,17 @@ Docs are published on cljdoc.org. Source articles live in `doc/`: - `doc/cljdoc.edn` — navigation tree (`{:cljdoc.doc/tree ...}`). - `doc/integrant.md`, `doc/example.md` — checked-in articles. -- `doc/tutorial/*.md` — **generated** from `test/darkleaf/di/tutorial/*.clj` - by `script/tutorial-to-md.sh`. Path is in `.gitignore`; the files only - exist in CI-built release commits, never on `master`. +- `doc/tutorial/*.md` and `doc/how_to/*.md` — **generated** from the + matching `test/darkleaf/di/{tutorial,how_to}/*.clj` files by + `script/tutorial-to-md.sh`. Paths are gitignored; the files only + exist in CI-built release commits. +- `doc/reference/*.md` — **plain tracked markdown**, not generated. + Reference pages are descriptive prose; verified examples for + whatever the page describes live in regular tests + (e.g. `dependency_types_test.clj` for the Factory protocol page). +- Planning artifacts from the v6 docs restructure live in + `doc/_journey.md` (chapter-by-chapter audit) and + `doc/_structure.md` (final TOC + rationale). ### Release flow @@ -72,11 +80,98 @@ Then trigger a rebuild on `https://cljdoc.org/d/org.clojars.darkleaf/di/X.Y.Z`. cljdoc rewrites markdown links between articles. Use either: -- relative to the source file: `tutorial/a_intro_test.md` (from `doc/example.md`) -- root-relative: `/doc/tutorial/a_intro_test.md` +- relative to the source file: `tutorial/a_your_first_system_test.md` (from `doc/example.md`) +- root-relative: `/doc/tutorial/a_your_first_system_test.md` -A bare `doc/tutorial/a_intro_test.md` (no leading slash) is **not** -recognised and renders as a broken external link. +A bare `doc/tutorial/a_your_first_system_test.md` (no leading slash) +is **not** recognised and renders as a broken external link. + +For an article-to-API-var link, use the full cljdoc URL with the +`CURRENT` placeholder — cljdoc rewrites `CURRENT` to the version +the reader is viewing: + +``` +[`di/->memoize`](https://cljdoc.org/d/org.clojars.darkleaf/di/CURRENT/api/darkleaf.di.core#->memoize) +``` + +Wikilinks (`[[ns/var]]`) only work inside docstrings, not in +articles. + +## Documentation conventions + +Settled during the v6 restructure. See `doc/_structure.md` for the +chapter-by-chapter rationale. + +### Audience + +- Primary: a Clojure developer familiar with Integrant or Component. +- Secondary: a Clojure developer who has never used a DI framework. + Don't assume Integrant knowledge in chapter bodies. +- Many readers are non-native English speakers. Optimise prose + for them: short sentences, no English `;` in prose (use + periods), no obscure idioms (`slip past it` is the kind that + trips people up; everyday phrasing like *side effect* or + *before any traffic* is fine). + +### Voice + +- Matter-of-fact, declarative. Match `doc/integrant.md` and + Stuart Sierra's *Reloaded Workflow* tone. No marketing language. +- No comparisons to other DI libraries inside tutorial or how-to + chapters. The Integrant comparison lives in `doc/integrant.md` + on its own. + +### Terminology + +- **Do not use the word "middleware"** in tutorial or how-to + chapters. Refer readers to `doc/reference/middleware_types.md` + when the concept is unavoidable. Talk about "arguments to + `di/start`" instead. +- **Math-style names** (`a`, `b`, `c`, …) are the project's + authorial style. Keep them — don't substitute concrete names + without a reason. +- **Keyword vs symbol** is about *intent*, not swap-ability. Both + can be substituted via the registry. A keyword means the author + decided to abstract the dependency (most commonly inside a + library or reusable internal module). A symbol points at a + specific var. + +### Directory layout + +- Tutorial chapters: `test/darkleaf/di/tutorial/[a-l]__test.clj`. + Letter prefix `a..l` matches chapter order (1–12) alphabetically. +- How-to recipes: `test/darkleaf/di/how_to/_test.clj`. No + order prefix. +- Reference pages: `doc/reference/.md` — plain markdown, + tracked. +- When adding a new doc subdirectory, also update + `script/tutorial-to-md.sh` (it iterates `tutorial`, `how_to`, + `reference`) and the `git add -f` line in + `.github/workflows/ci.yml` release job. + +### Test idioms in chapter files + +These conventions apply to tutorial and how-to chapter `.clj` +files. Regular tests under `test/darkleaf/di/*_test.clj` should +stay strict — prefer object-identity comparison there so subtle +regressions don't slip through. + +- Use `darkleaf.di.utils/catch-some` plus `ex-message` / `ex-data` + for exception assertions. Compare messages and structured data, + not exception objects by identity. +- Inline `(ex-info "..." {})` constructions where they fire — do + not pass exceptions through the registry just to assert on + them later. +- Add `;; ...` comments above non-obvious assertions to explain + what they verify. + +### Reference pages + +- A Reference page earns its keep when it adds material the + docstring does not: decision-trees, walks through code-level + patterns, design history, pitfall lists, aggregations across + multiple sources. +- Avoid duplicating the docstring 1-for-1. ## Release/build gotchas (not cljdoc-specific but related) From 71af1e16d00663191580d69c68fa874b1f60cef4 Mon Sep 17 00:00:00 2001 From: Mikhail Kuzmin Date: Sat, 27 Jun 2026 23:13:17 +0400 Subject: [PATCH 11/11] Rework the middleware reference into 'The middleware argument' Rename middleware_types.md to middleware_argument.md and rewrite it as a reference for the values di/start/inspect/->memoize accept: function, map, sequence, nil, and java.util.function.Function. Add the order/precedence rules, the ->memoize positioning constraint, and a note on why dispatch is a cond over predicates rather than a protocol. Update the cljdoc.edn TOC label to match. Co-Authored-By: Claude Opus 4.7 (1M context) --- doc/cljdoc.edn | 2 +- doc/reference/middleware_argument.md | 148 +++++++++++++++++++++++++++ doc/reference/middleware_types.md | 6 -- 3 files changed, 149 insertions(+), 7 deletions(-) create mode 100644 doc/reference/middleware_argument.md delete mode 100644 doc/reference/middleware_types.md diff --git a/doc/cljdoc.edn b/doc/cljdoc.edn index 3668f481..14d3e22f 100644 --- a/doc/cljdoc.edn +++ b/doc/cljdoc.edn @@ -26,6 +26,6 @@ ["Multiple systems" {:file "doc/how_to/multiple_systems_test.md"}] ["Two Databases" {:file "doc/tutorial/z_two_databases_test.md"}]]] ["Reference" - ["Middleware types" {:file "doc/reference/middleware_types.md"}]] + ["The middleware argument" {:file "doc/reference/middleware_argument.md"}]] ["Example app" {:file "doc/example.md"}] ["Integrant vs DI" {:file "doc/integrant.md"}]]} diff --git a/doc/reference/middleware_argument.md b/doc/reference/middleware_argument.md new file mode 100644 index 00000000..7d945896 --- /dev/null +++ b/doc/reference/middleware_argument.md @@ -0,0 +1,148 @@ +# The middleware argument + +`di/start`, `di/inspect` and `di/->memoize` all take a variadic +`middlewares` argument. A middleware wraps a registry to produce a +new one — see +[Design](/doc/design.md#middleware-wrapping-the-registry) for the +concept. This page describes the values the argument accepts: a +function, a map, a sequence, `nil`, or a +`java.util.function.Function`. + +## Function + +A function `registry -> key -> Factory`. The most general, and the +one the others reduce to. The inner function is the new registry, +so it either delegates to the one beneath it or answers with a +factory of its own: + +```clojure +;; pass every key through to the registry beneath +(fn middleware [registry] + (fn new-registry [key] + (registry key))) + +;; or answer with a factory of your own +(fn middleware [registry] + (fn new-registry [key] + (reify p/Factory + ...))) +``` + +Every built-in (`di/update-key`, `di/env-parsing`, `di/log`, +`di/ns-publics`, …) is a function of this shape. + +## Map + +A map of `{key factory}`. It overrides the listed keys and +delegates the rest to the registry beneath it: + +```clojure +(di/start `root {`db test-db ; a plain object, built as-is + ::clock (di/ref `fixed) ; a built-in Factory + `cache (reify p/Factory ; a hand-written Factory + ...) + "LOG_LEVEL" "debug"}) ; a plain value too +``` + +A value is used as the factory directly. A plain object counts as +a factory that builds to itself, so you can drop in a stub or a +literal without wrapping it; a value like `(di/ref ...)` or a +`reify p/Factory` is a factory in its own right. This is the most +common way to override components in tests and configuration. + +## Sequence + +A vector or seq of the other values, applied left to right and +flattened into the chain. It pairs naturally with `nil`: build the +sequence from `when` expressions, and the ones whose `when` is false +become `nil`, which is a no-op. + +```clojure +(defn dev-middlewares [{:keys [stub-db? verbose?]}] + [(when stub-db? {`db test-db}) + (when verbose? (di/log :after-build! report))]) + +(di/start `root (dev-middlewares flags) {::override :x}) +``` + +A sequence may contain any of these values, including other +sequences. + +## nil + +A no-op. Handy for a middleware that is only sometimes present, +without an `if` around the whole call: + +```clojure +(di/start `root (when dev? dev-middlewares)) +``` + +## java.util.function.Function + +A `java.util.function.Function` from registry to registry — the +same mapping as the function above, called through `.apply`. + +This exists for a stateful middleware that has to be more than +a function. `di/->memoize` returns a value that is both a `Function` +(so it works as a middleware) and `AutoCloseable` (so `di/stop` can +release everything it cached). A plain Clojure `fn` can't carry a +second interface like that, so the registry accepts a `Function` +object as an alternative. + +## Order + +Two directions are in play, and they are opposite. + +The values you pass are *applied* left to right: the leftmost wraps +the default registry, the next wraps that, and so on, so the +rightmost ends up outermost. A *lookup* then runs through that stack +from the outside in, so the rightmost middleware sees each key +first. When two of them answer the same key, the rightmost therefore +takes effect: + +```clojure +(di/start `root {`x :a} {`x :b}) ; `x resolves to :b +``` + +Keep in mind what a middleware actually does. A middleware only +chooses, per key, whether to answer or to delegate to the registry +beneath it. + +`di/->memoize` is constrained the other way: it must be the +*first* (leftmost) middleware. It does not wrap the registry it is +handed. It carries its own, built from the middleware passed to +`->memoize`, and rejects anything applied before it with +`::wrong-memoized-registry-position`. So anything that would sit +beneath `mem` has to be passed into it instead, and overrides go +after it, on the outside: + +```clojure +;; wrong — a middleware before mem is rejected +(di/start `root base-middlewares mem) ; throws + +;; right — fold them into mem, keep overrides on the outside +(def mem (di/->memoize base-middlewares)) +(di/start `root mem {::override :x}) ; ok +``` + +## Why a `cond`, not a protocol + +`di/start` dispatches over these values with a `cond` over standard +predicates, not a protocol. A protocol does not fit. You would have +to extend it onto maps, sequences and functions — abstract +groupings, not single types. A sequence alone covers lists, +vectors, lazy seqs, cons cells, and more. And the cases need an +explicit precedence that type-based dispatch does not give: a map +is also a collection, so it has to be recognised as a map of +overrides before the general sequence case. A `cond` over `map?`, +`sequential?`, `fn?` and `instance? Function` states both the +grouping and the order in one place. + +Because a function is already accepted, it doubles as the +extension point: to plug in a type of your own, convert it yourself +and pass the result, which is then an ordinary function or +`Function`. + +```clojure +(di/start `root (your-thing->middleware x)) +``` diff --git a/doc/reference/middleware_types.md b/doc/reference/middleware_types.md deleted file mode 100644 index 29f1987c..00000000 --- a/doc/reference/middleware_types.md +++ /dev/null @@ -1,6 +0,0 @@ -# Middleware types - -TODO: describe the registry-middleware abstraction -(`registry -> key -> Factory`) and list the built-in middlewares: -`di/update-key`, `di/env-parsing`, `di/add-side-dependency`, -`di/log`, `di/ns-publics`, the map form, and the seqable form.