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..1c72e531 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,7 @@ /target /pom.xml /doc/tutorial/ +/doc/how_to/ +#/doc/_*.md .clj-kondo/ .lsp/ \ No newline at end of file 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) 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..14d3e22f 100644 --- a/doc/cljdoc.edn +++ b/doc/cljdoc.edn @@ -3,24 +3,29 @@ ["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_your_first_system_test.md"}] + ["Dependencies" {:file "doc/tutorial/b_dependencies_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/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"}]] ["Advanced" - ["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"}] + ["Tips" {:file "doc/how_to/tips_test.md"}] + ["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"}] - ["Graceful stop" {:file "doc/tutorial/y_graceful_stop_test.md"}] - ["Multi arity service" {:file "doc/tutorial/y_multi_arity_service_test.md"}] - ["Multi system" {:file "doc/tutorial/z_multi_system_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"}] + ["Multiple systems" {:file "doc/how_to/multiple_systems_test.md"}] ["Two Databases" {:file "doc/tutorial/z_two_databases_test.md"}]]] + ["Reference" + ["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/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/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/why_di.md b/doc/why_di.md new file mode 100644 index 00000000..d3e8dc27 --- /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_your_first_system_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/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/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)))) 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/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)) 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))))) 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/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/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/tutorial/a_intro_test.clj b/test/darkleaf/di/tutorial/a_intro_test.clj deleted file mode 100644 index 18539c55..00000000 --- a/test/darkleaf/di/tutorial/a_intro_test.clj +++ /dev/null @@ -1,117 +0,0 @@ -;; # Intro - -(ns darkleaf.di.tutorial.a-intro-test - (:require - [clojure.test :as t] - [darkleaf.di.core :as di]) - (:import - (java.time Instant))) - -;; Let's start. -;; In this chapter I'll show you how to deal with components. - -;; ## Trivial system - -;; The following test describes the most trivial system that -;; contains the most trivial component. - -(def a ::a) - -;; `root` is a system root. -;; To get root's value deref it. -;; To stop a system use `di/stop`. - -(t/deftest a-test - (let [root (di/start `a)] - (t/is (= ::a @root)) - (di/stop root))) - -;; ## AutoCloseable - -;; A root implements `AutoCloseable` -;; so in tests we should use `with-open` macro -;; for properly stopping. - -(t/deftest a'-test - (with-open [root (di/start `a)] - (t/is (= ::a @root)))) - -;; ## Component - -;; A component definition is a function of 0 or 1 arity -;; with `{::di/kind :component}` meta. - -(defn b - {::di/kind :component} - [] - (Instant/now)) - -(t/deftest b-test - (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. - -(defn c - {::di/kind :component} - [-deps] - ::c) - -(t/deftest c-test - (with-open [root (di/start `c)] - (t/is (= ::c @root)))) - -;; ## Services - -;; A service is a function with or without dependencies. - -(defn d [] - ::d) - -;; `root` is a wrapper, and it implements `clojure.lang.IFn`, just like `clojure.lang.Var`. -;; So you can just call `root`. - -(t/deftest d-test - (with-open [root (di/start `d)] - (t/is (= ::d (@root) (root))))) - -(defn d* [-deps] - ::d) - -(t/deftest d*-test - (with-open [root (di/start `d*)] - (t/is (= ::d (@root) (root))))) - -(defn e [-deps arg] - [::e arg]) - -(t/deftest e-test - (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. - -(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/a_your_first_system_test.clj b/test/darkleaf/di/tutorial/a_your_first_system_test.clj new file mode 100644 index 00000000..787ca16f --- /dev/null +++ b/test/darkleaf/di/tutorial/a_your_first_system_test.clj @@ -0,0 +1,110 @@ +;; # Your first system + +(ns darkleaf.di.tutorial.a-your-first-system-test + (:require + [clojure.test :as t] + [darkleaf.di.core :as di]) + (:import + (java.time Instant))) + +;; 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. + + +;; ## 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) + +;; `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))) + +;; ## Stopping safely + +;; 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)))) + +;; ## Components + +;; 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} + [] + (Instant/now)) + +(t/deftest b-test + (with-open [root (di/start `b)] + (t/is (inst? @root)))) + +;; 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} + [-deps] + ::c) + +(t/deftest c-test + (with-open [root (di/start `c)] + (t/is (= ::c @root)))) + +;; ## Services + +;; A service is a plain `defn` — no metadata. DI does not call it +;; during start; the function itself is the component. + +(defn d [] + ::d) + +;; 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) + +(t/deftest d*-test + (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]) + +(t/deftest e-test + (with-open [root (di/start `e)] + (t/is (= [::e 42] (root 42))))) + +;; That's the vocabulary: system, root, components, and services. +;; The next chapter wires components together through real +;; dependencies. + diff --git a/test/darkleaf/di/tutorial/b_dependencies_test.clj b/test/darkleaf/di/tutorial/b_dependencies_test.clj index 63edd573..f535d466 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/e_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 deleted file mode 100644 index 1c812ed6..00000000 --- a/test/darkleaf/di/tutorial/c_stop_test.clj +++ /dev/null @@ -1,30 +0,0 @@ -;; # Stop - -(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. - -(defn root - {::di/stop #(reset! % true)} - [{::keys [*stopped?]}] - *stopped?) - -(t/deftest stop-test - (let [*stopped? (atom false)] - (with-open [root (di/start `root {::*stopped? *stopped?})] - (t/is (= false @@root))) - (t/is @*stopped?))) - -;; In most cases, a component will be a Java class. -;; To prevent reflection calls use `memfn` -;; ```clojure -;; (defn- connection-manager -;; {::di/stop (memfn ^AutoCloseable close)} -;; [{max-conn :env.long/CONNECTION_MANAGER_MAX_CONN -;; :or {max-conn 50}}] -;; (ConnectionManager. max-conn)) -;; ``` diff --git a/test/darkleaf/di/tutorial/c_stopping_components_test.clj b/test/darkleaf/di/tutorial/c_stopping_components_test.clj new file mode 100644 index 00000000..6f063977 --- /dev/null +++ b/test/darkleaf/di/tutorial/c_stopping_components_test.clj @@ -0,0 +1,52 @@ +;; # Stopping components + +(ns darkleaf.di.tutorial.c-stopping-components-test + (:require + [clojure.test :as t] + [darkleaf.di.core :as di])) + +;; 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)} + [{::keys [*stopped?]}] + *stopped?) + +(t/deftest stop-test + (let [*stopped? (atom false)] + (with-open [root (di/start `root {::*stopped? *stopped?})] + (t/is (= false @@root))) + (t/is @*stopped?))) + +;; ## 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-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/e_registries_test.clj b/test/darkleaf/di/tutorial/e_registries_test.clj new file mode 100644 index 00000000..90b57927 --- /dev/null +++ b/test/darkleaf/di/tutorial/e_registries_test.clj @@ -0,0 +1,70 @@ +;; # Registries + +(ns darkleaf.di.tutorial.e-registries-test + (:require + [clojure.test :as t] + [darkleaf.di.core :as di])) + +;; 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} + [{dep-a `dep-a + dep-b `dep-b}] + [:value dep-a dep-b]) + +;; ## 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-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)))) + +;; ## 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))) + ;; 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. + +(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.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/f_abstractions_test.clj b/test/darkleaf/di/tutorial/f_abstractions_test.clj new file mode 100644 index 00000000..167ca269 --- /dev/null +++ b/test/darkleaf/di/tutorial/f_abstractions_test.clj @@ -0,0 +1,103 @@ +;; # Abstractions + +(ns darkleaf.di.tutorial.f-abstractions-test + (:require + [clojure.test :as t] + [darkleaf.di.core :as di] + [darkleaf.di.utils :as u])) + +;; 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)) + +(defn get-current-user [{session ::session + get-user `get-user}] + (-> session :user-id get-user)) + +(defn ring-handler [{get-current-user `get-current-user} -req] + {:status 200 :body (str "Hi, " (get-current-user) "!")}) + +(t/deftest handler-test + (with-open [root (di/start `ring-handler + {::datasource {1 "John"} + ::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/g_environment_variables_test.clj b/test/darkleaf/di/tutorial/g_environment_variables_test.clj new file mode 100644 index 00000000..6670f996 --- /dev/null +++ b/test/darkleaf/di/tutorial/g_environment_variables_test.clj @@ -0,0 +1,108 @@ +;; # Environment variables + +(ns darkleaf.di.tutorial.g-environment-variables-test + (:require + [clojure.test :as t] + [darkleaf.di.core :as di])) + +;; 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} + [{path "PATH"}] + [:root path]) + +(def PATH (System/getenv "PATH")) + +(t/deftest root-test + (with-open [root (di/start `root)] + (t/is (= [:root PATH] @root)))) + +;; 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} + [{port :env.long/PORT + :or {port 8080}}] + [: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.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}] + [:enabled enabled]) + +(defn optional-env + {::di/kind :component} + [{enabled :env.bool/ENABLED + :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/h_starting_many_keys_test.clj b/test/darkleaf/di/tutorial/h_starting_many_keys_test.clj new file mode 100644 index 00000000..10f73362 --- /dev/null +++ b/test/darkleaf/di/tutorial/h_starting_many_keys_test.clj @@ -0,0 +1,43 @@ +;; # Starting many keys + +(ns darkleaf.di.tutorial.h-starting-many-keys-test + (:require + [darkleaf.di.core :as di] + [clojure.test :as t])) + +;; 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) + +;; ## Vector — Indexed 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)))) + +;; ## 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/i_wiring_inside_data_test.clj b/test/darkleaf/di/tutorial/i_wiring_inside_data_test.clj new file mode 100644 index 00000000..7d2891c6 --- /dev/null +++ b/test/darkleaf/di/tutorial/i_wiring_inside_data_test.clj @@ -0,0 +1,65 @@ +;; # Wiring inside data + +(ns darkleaf.di.tutorial.i-wiring-inside-data-test + (:require + [clojure.test :as t] + [darkleaf.di.core :as di])) + +;; 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. + +;; ## Embedding refs in 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 (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/j_transforming_values_test.clj b/test/darkleaf/di/tutorial/j_transforming_values_test.clj new file mode 100644 index 00000000..e99cd894 --- /dev/null +++ b/test/darkleaf/di/tutorial/j_transforming_values_test.clj @@ -0,0 +1,63 @@ +;; # Transforming values + +(ns darkleaf.di.tutorial.j-transforming-values-test + (:require + [clojure.test :as t] + [darkleaf.di.core :as di])) + +;; 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: + +;; ```clojure +;; (di/derive source-key f arg1 arg2 ...) +;; ;; ≡ (f source-value arg1 arg2 ...) +;; ``` + +;; ## 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: + +(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/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 +;; 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) + (di/opt-ref ::c)])) +(def box (di/derive `-box (partial filter some?))) + +(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/k_composition_with_update_key_test.clj b/test/darkleaf/di/tutorial/k_composition_with_update_key_test.clj new file mode 100644 index 00000000..3b7cc31d --- /dev/null +++ b/test/darkleaf/di/tutorial/k_composition_with_update_key_test.clj @@ -0,0 +1,82 @@ +;; # Composition with `update-key` + +(ns darkleaf.di.tutorial.k-composition-with-update-key-test + (:require + [clojure.test :as t] + [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 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. + +(defn handler [-deps req] + {:status 200 :body (:uri req)}) + +(defn wrap-log [handler *log] + (fn [req] + (swap! *log conj (:uri req)) + (handler req))) + +(t/deftest decorate-test + (let [*log (atom [])] + (with-open [root (di/start `handler + (di/update-key `handler wrap-log *log))] + (t/is (= {:status 200 :body "/a"} (root {:uri "/a"}))) + (t/is (= {:status 200 :body "/b"} (root {:uri "/b"}))) + (t/is (= ["/a" "/b"] @*log))))) + +;; ## 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. + +(defn user-handler [-deps -req] + :user) + +(def user-route (di/template ["/users" (di/ref `user-handler)])) + + +(defn order-handler [-deps -req] + :order) + +(def order-route (di/template ["/orders" (di/ref `order-handler)])) + + +(def routes []) + +(t/deftest extend-test + (with-open [root (di/start `routes + (di/update-key `routes conj (di/ref `user-route)) + (di/update-key `routes conj (di/ref `order-route)))] + (t/is (= [["/users" :user] + ["/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.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. diff --git a/test/darkleaf/di/tutorial/l_handling_start_failures_test.clj b/test/darkleaf/di/tutorial/l_handling_start_failures_test.clj new file mode 100644 index 00000000..4b2e7fe9 --- /dev/null +++ b/test/darkleaf/di/tutorial/l_handling_start_failures_test.clj @@ -0,0 +1,79 @@ +;; # Handling start failures + +(ns darkleaf.di.tutorial.l-handling-start-failures-test + (:require + [clojure.test :as t] + [darkleaf.di.core :as di] + [darkleaf.di.utils :refer [catch-some]])) + +;; 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. + +;; 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}] + (throw (ex-info "build failed" {}))) + +;; `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 (= "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. diff --git a/test/darkleaf/di/tutorial/l_registries_test.clj b/test/darkleaf/di/tutorial/l_registries_test.clj deleted file mode 100644 index 680e548b..00000000 --- a/test/darkleaf/di/tutorial/l_registries_test.clj +++ /dev/null @@ -1,41 +0,0 @@ -;; # Registries - -(ns darkleaf.di.tutorial.l-registries-test - (:require - [clojure.test :as t] - [darkleaf.di.core :as di])) - -;; Here we use undefined dependencies. - -(defn value - {::di/kind :component} - [{dep-a `dep-a - dep-b `dep-b}] - [:value dep-a dep-b]) - -;; To locally define or redefine a dependency we should use registries. - -(t/deftest map-registry - (with-open [root (di/start `value {`dep-a :a `dep-b :b})] - (t/is (= [:value :a :b] @root))) - - (with-open [root (di/start `value {`value :replacement})] - (t/is (= :replacement @root))) - - (with-open [root (di/start `value {`dep-a :a} {`dep-b :b})] - (t/is (= [:value :a :b] @root))) - - ;; last 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)))) - - -;; To avoid using `(apply di/start ...)`, -;; we can use a seqable value as a single registry. -;; See `clojure.core/seqable?`. -(t/deftest seqable-registry - (with-open [root (di/start `value [{`dep-a :a} - [{`dep-b :b}]])] - (t/is (= [:value :a :b] @root)))) diff --git a/test/darkleaf/di/tutorial/m_abstractions_test.clj b/test/darkleaf/di/tutorial/m_abstractions_test.clj deleted file mode 100644 index 3350871d..00000000 --- a/test/darkleaf/di/tutorial/m_abstractions_test.clj +++ /dev/null @@ -1,27 +0,0 @@ -;; # Abstractions - -(ns darkleaf.di.tutorial.m-abstractions-test - (:require - [clojure.test :as t] - [darkleaf.di.core :as di])) - -;; 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. - -(defn get-user [{ds ::datasource} id] - (ds id)) - -(defn get-current-user [{session ::session - get-user `get-user}] - (-> session :user-id get-user)) - -(defn ring-handler [{get-current-user `get-current-user} -req] - {:status 200 :body (str "Hi, " (get-current-user) "!")}) - -(t/deftest handler-test - (with-open [root (di/start `ring-handler - {::datasource {1 "John"} - ::session {:user-id 1}})] - (t/is (= {:status 200 :body "Hi, John!"} - (root {}))))) diff --git a/test/darkleaf/di/tutorial/n_env_test.clj b/test/darkleaf/di/tutorial/n_env_test.clj deleted file mode 100644 index ad08b2af..00000000 --- a/test/darkleaf/di/tutorial/n_env_test.clj +++ /dev/null @@ -1,72 +0,0 @@ -;; # Env - -(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. - -(defn root - {::di/kind :component} - [{path "PATH"}] - [:root path]) - -(def PATH (System/getenv "PATH")) - -(t/deftest root-test - (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. - -(defn jetty - {::di/kind :component} - [{port :env.long/PORT - :or {port 8080}}] - [:jetty port]) - -(t/deftest jetty-test - (with-open [jetty (di/start `jetty - (di/env-parsing {:env.long parse-long}))] - (t/is (= [:jetty 8080] @jetty))) - (with-open [jetty (di/start `jetty - (di/env-parsing :env.long parse-long) - {"PORT" "8081"})] - (t/is (= [:jetty 8081] @jetty)))) - -(defn required-env - {::di/kind :component} - [{enabled :env.bool/ENABLED}] - [:enabled enabled]) - -(defn optional-env - {::di/kind :component} - [{enabled :env.bool/ENABLED - :or {enabled true}}] - [:enabled enabled]) - -(t/deftest env-test - (t/is (thrown? clojure.lang.ExceptionInfo - (di/start `required-env - (di/env-parsing {:env.bool #(= "true" %)})))) - - (with-open [sys (di/start `required-env - (di/env-parsing {:env.bool #(= "true" %)}) - {"ENABLED" "false"})] - (t/is (= [:enabled false] @sys))) - - (with-open [sys (di/start `optional-env - (di/env-parsing {:env.bool #(= "true" %)}))] - (t/is (= [:enabled true] @sys))) - - (with-open [sys (di/start `optional-env - (di/env-parsing {:env.bool #(= "true" %)}) - {"ENABLED" "false"})] - (t/is (= [:enabled false] @sys)))) diff --git a/test/darkleaf/di/tutorial/o_data_dsl_test.clj b/test/darkleaf/di/tutorial/o_data_dsl_test.clj deleted file mode 100644 index 750b2178..00000000 --- a/test/darkleaf/di/tutorial/o_data_dsl_test.clj +++ /dev/null @@ -1,27 +0,0 @@ -;; # Data DSL - -(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`. - -;; You can also use `di/opt-ref` for optional dependencies. -;; If there is no defined key opt-ref resolves to nil. - -(def route-data - (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))))) diff --git a/test/darkleaf/di/tutorial/p_derive_test.clj b/test/darkleaf/di/tutorial/p_derive_test.clj deleted file mode 100644 index 37bd7574..00000000 --- a/test/darkleaf/di/tutorial/p_derive_test.clj +++ /dev/null @@ -1,39 +0,0 @@ -;; # Derive - -(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. - -;; The first way - -(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)))) - - -;; The second way - -(def port' (di/derive "PORT" parse-long)) - -(t/deftest port'-test - (with-open [root (di/start `port' {"PORT" "8080"})] - (t/is (= 8080 @root)))) - - -(def -box (di/template [(di/opt-ref ::a) - (di/opt-ref ::b) - (di/opt-ref ::c)])) -(def box (di/derive `-box (partial filter some?))) - -(t/deftest box-test - (with-open [root (di/start `box {::b :b})] - (t/is (= [:b] @root)))) diff --git a/test/darkleaf/di/tutorial/q_starting_many_keys_test.clj b/test/darkleaf/di/tutorial/q_starting_many_keys_test.clj deleted file mode 100644 index 3fd1b6b9..00000000 --- a/test/darkleaf/di/tutorial/q_starting_many_keys_test.clj +++ /dev/null @@ -1,33 +0,0 @@ -;; # Starting many keys - -(ns darkleaf.di.tutorial.q-starting-many-keys-test - (:require - [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. - -(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)))) - -;; The root container implements `clojure.lang.Indexed` -;; so you can use destructuring without derefing the root. - -(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. - -(t/deftest lookup-test - (di/with-open [{:keys [a b]} (di/start {:a `a :b `b})] - (t/is (= :a a)) - (t/is (= :b b)))) 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))))) 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. diff --git a/test/darkleaf/di/tutorial/x_update_key_test.clj b/test/darkleaf/di/tutorial/x_update_key_test.clj deleted file mode 100644 index 8a6addb2..00000000 --- a/test/darkleaf/di/tutorial/x_update_key_test.clj +++ /dev/null @@ -1,75 +0,0 @@ -;; # Update key - -(ns darkleaf.di.tutorial.x-update-key-test - (:require - [clojure.test :as t] - [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)). - -;; ## 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. - -(defn handler [-deps req] - {:status 200 :body (:uri req)}) - -(defn wrap-log [handler *log] - (fn [req] - (swap! *log conj (:uri req)) - (handler req))) - -(t/deftest decorate-test - (let [*log (atom [])] - (with-open [root (di/start `handler - (di/update-key `handler wrap-log *log))] - (t/is (= {:status 200 :body "/a"} (root {:uri "/a"}))) - (t/is (= {:status 200 :body "/b"} (root {:uri "/b"}))) - (t/is (= ["/a" "/b"] @*log))))) - -;; ## 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. - -(defn user-handler [-deps -req] - :user) - -(def user-route (di/template ["/users" (di/ref `user-handler)])) - - -(defn order-handler [-deps -req] - :order) - -(def order-route (di/template ["/orders" (di/ref `order-handler)])) - - -(def routes []) - -(t/deftest extend-test - (with-open [root (di/start `routes - (di/update-key `routes conj (di/ref `user-route)) - (di/update-key `routes conj (di/ref `order-route)))] - (t/is (= [["/users" :user] - ["/orders" :order]] - (for [[path handler] @root] - [path (handler :req)]))))) diff --git a/test/darkleaf/di/tutorial/y_graceful_stop_test.clj b/test/darkleaf/di/tutorial/y_graceful_stop_test.clj deleted file mode 100644 index b85fd8b2..00000000 --- a/test/darkleaf/di/tutorial/y_graceful_stop_test.clj +++ /dev/null @@ -1,37 +0,0 @@ -;; # Graceful stop - -(ns darkleaf.di.tutorial.y-graceful-stop-test - (:require - [clojure.test :as t] - [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. - -;; It throws the original exception. -;; All other possible exceptions are added as suppressed. - -(defn root - {::di/kind :component} - [{dep `dep - on-start-root-ex ::on-start-root-ex}] - (throw on-start-root-ex)) - -(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)] - (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)))))