From 134dd09547ba3d1a70ec68a898b0562884851f65 Mon Sep 17 00:00:00 2001 From: Artem Sheptunov <106321977+Infindery@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:57:20 +0500 Subject: [PATCH 01/19] docs: add a draft article --- docs/article.md | 184 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 docs/article.md diff --git a/docs/article.md b/docs/article.md new file mode 100644 index 0000000..de74743 --- /dev/null +++ b/docs/article.md @@ -0,0 +1,184 @@ +На кого нацелена статья: +- Backend разработчики +- C++ разработчики + +Цель: +Рассказать о том, как мы создали переиспользуемый шаблон API на C++. + +Задачи: +- Рассказать, почему мы решили попробовать написать шаблон API на C++. +- Рассказать, какие инструменты для разработки мы нашли и почему выбрали определённые. +- Рассказать, какой проект мы хотим реализовать в качестве шаблона. +- Рассказать, какие подводные камни нам встретились по ходу разработки. + +Темы, которые нужно осветить: +- Желание получить шаблон API на C++. +- Желание построить опыт работы с шаблоном подобно create-react-app. [!] +- Отсутствие готовых решений, статей и других ресурсов на поверхности. +- Потенциал для встраивания библиотек с другим рантаймом. [!] +- Простота использования. Получил бинарник -> используешь. [!] +- Полная власть над проектом, потенциал хорошего перформанса. +- Выбор пакетного менеджера, пакетный менеджер Conan. +- Выбор компилятора, компилятор Clang. +- Выбор фреймворка, фреймворк Drogon. +- Желание избежать прямых SQL-запросов, ORM ODB. +- Отсутствие conan-рецептов для libodb и libodb-pgsql. +- Отсутствие функционала для миграций базы данных, внедрение Alembic. + +[ ------------- ] + +# Как создали переиспользуемый шаблон API на C++. + +В нашей компании мы создаём программные продукты, используя широкий спектр технологий и не ограничиваясь одним стеком. Сегодня в нашем арсенале уже есть .NET, JavaScript/TypeScript, С/C++, Python и множество инструментов - от React, Next.js и NestJS до .NET, Python, Kubernetes, kind и S3, и это лишь часть того, с чем мы работаем ежедневно. + +Но для нас важно не только применять уже полученные знания, но постоянно расширять горизонты: осваивать новые языки, пробовать новые инструменты и совершенствовать наши процессы. Одной из таких инициатив стало наше давнее желание попробовать C++ в бекенд разработке, использовав его для реализации шаблона API в виде приложения для управления задачами - To-dos. + +В нашем GitHub уже представлен пример такого приложения, которое было написано на TypeScript`е с использованием инструмента NestJS, поэтому задача казалась не такой сложной - нужно лишь повторить то, что уже сделано, но только на C++. К тому же, помимо API, у нас уже есть готовый UI, а также локальный environment, который мы называем local-env, в лице кластера kind. + +Кстати, наш руководитель Александр проводил воркшоп на тему TDD разработки на фронтенде, где в качестве примера использовался вышеупомянутый UI, а также выступал с докладом на <название конфы> про local-env. Очень рекомендуем ознакомиться. + +Может возникнуть вопрос, а зачем вообще пытаться реализовать backend на C++? Ведь кажется, что это не самый очевидный и популярный в сообществе язык для подобных целей и можно было бы использовать что-нибудь другое. + +Однако у C++ есть свои сильные стороны, так благодаря своей специфике он даёт полный контроль над ресурсами и производительностью приложения. Это открывает возможности для создания высоконагруженных API с тонкой оптимизацией, где важны скорость и эффективность работы. В результате можно добиться не только высокой производительности, но и качественного пользовательского опыта при взаимодействии с таким сервисом. + +Так вот, цель поставлена, пример есть, остаётся только выбрать инструменты и реализовать проект. + +<---> + +Сперва мы решили не бросаться во все тяжкие, а сначала поискать уже существующие примеры - статьи, доклады, а может, и готовые проекты, которые могли бы ускорить наш старт и заранее подсветить возможные подводные камни. Однако довольно быстро стало понятно, что на поверхности информации крайне мало. Материалов по разработке backend`а на C++ оказалось заметно меньше, чем мы думали, а подобные open source проекты и вовсе редкость. + +Вместо того чтобы разочароваться, мы увидели в этом возможность, ведь стало понятно, что тема явно недостаточно освещена в сообществе, а значит, наша работа может принести реальную пользу. Тем более, что ещё на этапе идеи мы планировали делать проект открытым и делиться полученным опытом с другими. + +<---> + +Как мы уже упоминали, опыт работы с C/C++ у нас был и раньше. Однако при использовании сторонних библиотек мы придерживались довольно консервативного подхода. Скачать исходники, собрать проект под нужную конфигурацию, слинковать с собственным бинарником, повторить шаг первый... Думаем, многим это знакомо. + +Но, как это часто бывает, начинаешь по-настоящему ценить удобство, только попробовав альтернативы. Поработав с другими языками и их экосистемами, где управление зависимостями давно упрощено с помощью пакетных менеджеров, мы невольно начали скучать по такому же уровню комфорта. Возвращаясь к C++, мы задались вполне логичным вопросом - а есть ли здесь что-то сопоставимое с npm, pip или cargo? + +И тут мы столкнулись с особенностью, исторически для C/C++ так и не сформировалось единого, общепринятого пакетного менеджера, аналогичного тем, что существуют в других языках. Кто-то использует vcpkg, кто-то Conan, а кто-то продолжает повторять шаг первый, второй и третий (то, как действовали мы, упоминая об этом ранее). + +Мы решили попробовать внедрить один из существующих пакетных менеджеров в наш проект, поэтому сформировали несколько требований для его выбора, а именно: +- **Простота в использовании**. + + Установка пакетного менеджера и первоначальная конфигурация должна быть простой. Кроме того, в своей работе мы активно используем VSCode DevContainers, поэтому интеграция с подобной средой должна была быть возможна. + + Команды и общий workflow должны быть интуитивно понятными, чтобы не повышать порог входа в проект. + + Пакетный менеджер должен иметь обширную документацию, желательно с примерами. +- **Пакетная экосистема**. + + В библиотеке менеджера должно быть представлено достаточное количество пакетов, чтобы можно было вести разработку. + + Библиотека менеджера должна содержать пакеты для актуальных инструментов (такие как, веб-фреймворки, ORM и тп.). +- **Интеграция с системами сборки**. + + Поддержка CMake, Make и других систем сборки, а также поддержка сборки внутри docker-контейнера. + + Простая интеграция без необходимости изменять существующие скрипты сборки. + + Генерация конфигурационных файлов. +- **Скорость работы**. + + Быстрая загрузка и установка зависимостей (имеет значение при пересборке контейнера). + +Определившись с требованиями, мы начали искать существующее решение, удалетворяющее озвученным требованиям. Почти сразу на наш стол попали vcpkg и Conan, которые, как нам в тот момент показалось, пользовались большей популярностью в комьюнити разработчиков. Каждый из них в той или иной мере удолетворял нашим требованиям, но также и имел свои особенности. + +Так, оба инструмента были в равной степени просты в установке, имели интуитивно понятный набор команд, обладали исчерпывающий документацией и большим объемом библиотеки пакетов. Различия начались с организации хранения пакетов. Тогда, как Conan обладал децентрализованной организацией хранения, что позволяет хранить пакеты в удалённом индексе или локально, vcpkg имел организацию централизованную на удалённом индексе, принадлежащим Microsoft. + +Мы составили таблицу, в которой провели сравнение этих инструментов и вот, что у нас получилось: + +| Критерий | **Conan** | **vcpkg** | +|-------------------------------------|------------------------------------|----------------------------------| +| Простота установки | + | + | +| Интуитивно понятный | + | + | +| Обширная документация | + | + | +| Объём библиотеки пакетов | + | + | +| Хранение пакетов | + (децентрализованное) | + (централизованное) | +| Интеграция с системами сборки | + (любая) | + (CMake, MSBuild) | +| Простота интеграции | + (без принудительного toolchain) | - (обязательный CMake toolchain) | +| Поддержка бинарных пакетов | + (CI/CD, пресборка devcontainer) | + (менее гибко) | +| Поддержка пакетов с исходным кодом | + (очень гибко) | + (ограниченно) | + +Поскольку изначально мы рассматривали этот проект как шаблон для последующей разработки на C++, для нас было важно выбирать максимально универсальные инструменты. + +Не секрет, что vcpkg развивается при поддержке Microsoft и, несмотря на свою кроссплатформенность, лучше всего раскрывается при работе на Windows, особенно в связке с Visual Studio. В других окружениях его использование также возможно, но нередко требует дополнительной настройки. + +Кроме того, vcpkg в ориентирует на использование CMake toolchain или интеграцию с MSBuild через Visual Studio. Несмотря на то, что мы также рассматривали CMake в качестве основной системы сборки, возможность Conan работать с разными системами показалась нам более универсальным решением. + +Как вы возможно уже догадались, наш выбор пал на Conan. Именно его мы встроили и использовали в нашем проекте. + +Блягодаря внедрению Conan мы смогли упростить наш опыт работы с зависимостями проекта, теперь установка и сборка происходила под капотом, а нам оставалось лишь указать нужную зависимость в конфигурационном файле и запустить процесс. + +Проблема была решена и тогда мы решили, что сложностей больше не должно возникать, по крайней мере нам так казалось на тот момент, но об этом позднее. + +<---> + +Следующее, что нам нужно было сделать - выбрать компилятор, который бы мы использовали в проекте. В моменте сразу же промелькнула мысль об использовании компиляторов GCC или Clang, MSVC мы не рассматривали, поскольку не хотели привязываться к определённой платформе или IDE, как упоминали ранее. + +Изначально нам казалось, что для наших задач оптимальным выбором будет GCC, а Clang воспринимался скорее как инструмент из экосистемы Apple. Однако, углубившись в вопрос кроссплатформенной сборки, мы пересмотрели этот взгляд. + +Оказалось, что Clang в ряде сценариев может быть более удобным выбором, так при использовании GCC для кросс-компиляции зачастую требуется установка отдельных компиляторов под каждую целевую архитектуру, тогда как в случае с Clang многое доступно «из коробки» и достаточно лишь настроить линковщик и стандартные библиотеки. + +Кроме этого, Clang поставляется в составе LLVM, который включает в себя и другие полезные инструменты, например, clang-tidy для статического анализа кода и clang-format для автоматического форматирования, которые мы также смогли использовать в рамках нашего проекта. + +Для того, чтобы Conan использовал нужный компилятор, с целевой версией стандарта C++, а также собирал под нужную архитектуру с использованием правильного линковщика и статических библиотек, необходимо явно сказать ему об этом. Для подобных целей сущестуют профили Conan. Для наших целей мы опредлили в проекте 3 профиля, где указали нужные конфигурации для каждой из целевых платформ. Так у нас получился профиль для архитектуры amd64, для архитектуры arm64 и универсальный для разработки, в котором мы использовали возможности шаблонизатора Jinja2. Шаблонизатор позволил нам автоматически опредлять архитуктуру и платформу, что упрощает общий опыт разработки, особенно внутри devcontainer. + +Важно учитывать, что для кроссплатформенной работы Conan необходимо явно определить параметры сборки, такие как используемый компилятор, целевую версию стандарта C++, архитектуру, линковщик, тип подключаемых библиотек и т.п. Для этого в Conan используюется механизм профилей. + + +В рамках проекта мы определили три профиля с нужными конфигурациями под разные сценарии. Сейчас у нас есть отдельные профили для архитектур amd64 и arm64, а также универсальный профиль для разработки. В последнем мы использовали возможности шаблонизатора Jinja2, что позволило автоматически определять архитектуру и платформу, что упростило процесс разработки, особенно при работе внутри VSCode DevContainer. + +<---> + +К веб-фреймворку у нас было не так много требований, как к пакетному менеджеру, но большинство являлось очень важными. + +Так мы хотели найти решение, которое бы удолетворяло следующим требованиям: +- **Простота в использовании** + + Установка фреймворка и его настройка должна быть простой. + + Чистый и понятный синтаксис. + + Простая работа с query-параметрами, заголовкам и телом запроса. + + Обширная документация с примерами. +- **Возможности** + + Реализация middlewares в том числе для аунтификации. + + Работа в мультипоточном режиме, паралелльная обработка запросов. +- **ORM** + + Наличие собственной системы ORM, избавляющей от необходимости работать напрямую с SQL. + + Возможность реализации функционала работы с миграциями базы данных. + +Мы выбрали несколько вариантов - userver, Oat++ и Drogon, которые нам показались более актуальными. + +В ходе работы мы поочерёдно попытались внедрить каждый из них в проект по отдельности и реализовывали минимальный набор функциональности для того, чтобы оценить опыт использования каждого, а также понять, насколько каждый из фреймворков соответствует нашим требованиям. + +Результирующая таблица сравнения, которая нас получилось: + +| Критерий | Drogon | userver | Oat++ | +| ---------------------------| ----------------- | --------------------------- | --------------------- | +| Простота установки | + | - | + | +| Читаемый синтаксис | + | - | + (но сложнее Drogon) | +| Простая работа с запросами | + | (исключили из эксперимента) | + | +| Обширная документация | + (но с минусами) | (исключили из эксперимента) | + | +| Работа с middleware | + | (исключили из эксперимента) | + | +| Мультипоточная работа | + | (исключили из эксперимента) | + | +| Наличие high-level ORM | - | (исключили из эксперимента) | - | +| Возможность миграций | - | (исключили из эксперимента) | - | + +На определённый момент мы столкнулись с тем, что установка userver оказалась довольно сложной, официальная документация в основном ориентировала нас на копирование шаблонного репозитория перед созданием собственного проекта. Кроме того, нам показалось, что синтаксис userver достаточно сложный. В итоге мы решили исключить его из дальнейших экспериментов. + + +В случае Drogon и Oat++ установка оказалась заметно проще, поскольку оба пакета были представлены в conan-center-index (хранилище пакетов Conan), поэтому мы решили продолжить проводить эксперименты только с ними. + + +Оба фреймворка, на наш взгляд, обладали простым и понятным синтаксисом, однако в Drogon он выглядел более прозрачным, так как не был обёрнут в кодовые макросы, также оба позволяли удобно работать с запросами и middlewares. + +Drogon, как и Oat++, имеет обширную документацию, однако иногда встречаются ссылки на разделы, которых по непонятным причинам нет в актуальной версии документации. Это порой приводит к затруднениям. + + +Если говорить о мультипоточной работе, то оба фреймворка позволяют использовать несколько потоков и обрабатывать запросы параллельно. + +Также для нас было важно, чтобы веб-фреймворк имел богатую методами собственную high-level ORM, чтобы избежать необходимости написания прямых SQL запросов, а также чтобы фреймворк поддерживал работу с миграциями базы данных. Кроме того, поскольку в своих проектах мы используем PostgreSQL, ORM также должна быть совместима с ней. + +И Drogon, и Oat++ имеют собственный ORM фреймворк. + +Oat++ Предоставляет разработчику набор макросов для работы с базой данных и поразумевает написание разработчиком собственных SQL-запросов. + +В случае Drogon, фреймворк предоставляет набор методов для выполнения базовых команд покрывающих операции CRUD, но для более сложных задач также ориентирует на написание собственных SQL-запросов. Также, подразумевается генерация классов моделей по предварительно подготовленной базе данных, то есть это означает, что база данных и таблицы должны быть предварительно созданы разработчиком и только после этого drogon_ctl сможет сгенерировать классы моделей. + + +Также, оба фреймворка не поддерживают работу с миграциями базы данных. Всё это подтолкнуло нас к поиску сторонних инструментов для решения этой проблемы. + +<---> + +В процессе поиска мы наткнулись на несколько иснтрументов - ODB и TinyORM. + +Если говорить о TinyORM, то он предоставляет средства управления типами связей и синтаксис запросов в стиле LINQ. From 3c7cbe16da0644c6fb60c64b69d521bc34f60fac Mon Sep 17 00:00:00 2001 From: Artem Sheptunov <106321977+Infindery@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:37:24 +0500 Subject: [PATCH 02/19] docs: #71: update article --- docs/article.md | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/docs/article.md b/docs/article.md index de74743..b588b20 100644 --- a/docs/article.md +++ b/docs/article.md @@ -25,9 +25,43 @@ - Отсутствие conan-рецептов для libodb и libodb-pgsql. - Отсутствие функционала для миграций базы данных, внедрение Alembic. -[ ------------- ] +[ --- ] + +# Взять и использовать или как мы упростили себе жизнь при разработке на C++ + +Представьте, что вы решили начать разработку нового API С++ бекенда и вместо того, чтобы создавать новый репозиторий, искать подходящие инструменты и настраивать всю инфраструктуру с нуля, нажимаете пару кнопок и шаблон для вашего будущего API с настроенной инфраструктурой уже готов. + +Представили? Мы тоже и ограничиваться одним только представлением не стали, поэтому в серьез задумались об воплощении такого шаблона в жизнь. К тому же, как и всегда, всю разработку вели open-source, поэтому каждый из вас может просто "взять и использовать" этот шаблон для своих целей. И это не просто красивые слова - взять его можно [тут](https://github.com/TourmalineCore/to-dos-api-cpp), а использовать [так](https://github.com/TourmalineCore/to-dos-api-cpp/blob/master/README.md). + +А мы дальше расскажем, как рождался этот шаблон, почему он устроен именно так, и через какие подводные камни нам пришлось пройти, чтобы каждый читатель мог взять этот шаблон и уверенно стартовать свой проект, не тратя время на настройку необходимой инфраструктуры и не наступая на те же грабли. + +< --- > + +Всё начиналось с того, что нам потребовался шаблон API бэкенда на C++. Как известно, благодаря своей специфике, C++ предоставляет разработчику полный контроль над ресурсами, что открывает возможности для создания высоконагруженных API с тонкой оптимизацией, где важны скорость и эффективность работы. + +В закромах своего GitHub мы уже имеем несколько шаблонов на разных языках, которые вместе реализуют приложение для управления задачами - To-Dos. Кстати, один из них использовался в качестве примера в докладе нашего руководителя Александра, который проводил воркшоп на тему TDD разработки на фронтенде. + +Мы решили реализовать что-то похожее но для C++, чтобы при необходимости не тратить время на одну и ту же рутину по типу настройки сборки или конфигурации окружения, а подобно create-react-app, когда одной команды в терминале достаточно, чтобы перед тобой появилась готовая структура проекта со всем необходимым, просто взять шаблон и сразу писать код. + +< --- > + +"Взять и использовать" звучит как "легко и просто" и по нашему мнению в современной разработке все должно следовать этому подходу, по крайней мере, нам, как разработчикам, этого бы хотелось. Но "легко и просто" разбивается о реальность, когда заходит речь о системе сборки и работы с зависимостями в C++. + +Так повелось, что в комьюнити C/C++ разработчиков не сформировалось единого подхода к сборке проекта и управлению зависимостями, в отличие от JavaScript, Python и многих других языков. Одни связывают это с возрастом языка, когда C только появился, самой идеи пакетного менеджера ещё не существовало. Другие, с тем, что C++ компилируется в нативный код, а значит одна и та же библиотека под разные платформы и архитектуры ведёт себя по-разному. Скорее всего, правы и те, и другие. + +Но нас, как разработчиков, исторический экскурс интересует меньше, чем конкретный вопрос - как выстроить комфортный рабочий процесс в подобных агрессивных условиях? + + + +Наверное, у каждый разработчик перед тем, как начать писать API backend на C++, задаётся вопросом - а не делаю ли я ошибку, выбирая такой язык, как C++, для реализации подобного типа проекта? Не будет ли правильнее взять что-то более современное и устоявшееся, как например dotnet, JavaScript или Python? И если говорить на чистоту, то в большинстве случаев сменить язык будет правильным выбором, но, несмотря на это, С++ имеет свои сильные стороны. + +Так, благодаря своей специфике, C++ предоставляет разработчику полный контроль над ресурсами и производительностью приложения, что открывает возможности для создания высоконагруженных API с тонкой оптимизацией, где важны скорость и эффективность работы. В результате можно добиться не только высокой производительности, но и качественного пользовательского опыта при взаимодействии с таким сервисом. + +Взять и использовать - легко и просто, кажется, что в современной разработке все должно следовать этому подходу, по крайней мере, нам, как разработчикам, этого бы хотелось. Но что делать, когда история вносит в процесс свою директиву, особенно когда речь идёт о разработке на C++. Так повелось, что в комьюнити C/C++ разработчиков не сформировалось единого подхода к сборке проекта и работы с его зависимостями, как это произошло в JavaScript, Python и других языках. Некоторые говорят, что это связано с тем, что на момент появления языка, такого запроса не было, некоторые что то-то. Все эти утверждения носят правдивый характер, но как действовать нам, разработчикам, в условиях агрессивной среды, когда хочется наладить процесс нашей работы так, чтобы всё было легко и просто. + +именно этот слоган будет лежать в основе дальшейнего повествования. + -# Как создали переиспользуемый шаблон API на C++. В нашей компании мы создаём программные продукты, используя широкий спектр технологий и не ограничиваясь одним стеком. Сегодня в нашем арсенале уже есть .NET, JavaScript/TypeScript, С/C++, Python и множество инструментов - от React, Next.js и NestJS до .NET, Python, Kubernetes, kind и S3, и это лишь часть того, с чем мы работаем ежедневно. From fd9ef2cecd375f04ae47bdae9860fa8dffb7ad88 Mon Sep 17 00:00:00 2001 From: Oleg Kl Date: Mon, 29 Jun 2026 18:57:59 +0500 Subject: [PATCH 03/19] docs: #71: add chapter about issues --- docs/article.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/article.md b/docs/article.md index b588b20..362304a 100644 --- a/docs/article.md +++ b/docs/article.md @@ -216,3 +216,29 @@ Oat++ Предоставляет разработчику набор макро В процессе поиска мы наткнулись на несколько иснтрументов - ODB и TinyORM. Если говорить о TinyORM, то он предоставляет средства управления типами связей и синтаксис запросов в стиле LINQ. + + + +### Первый камень +Наверное один из самых насущных подводных камней оказалось отсутсвие необходимых пакетов в `conan-center-index`. В ходе поэтапного выбора необходимых инструментов внутри шаблона мы наконец-то подошли к выбору ORM. И самой, на нащ взгляд, перспективной библиотеки ODB для работы с БД не оказалось в `conan-center-index`. +И как бы мы не хотели, но просто нажать и запустить шаблон не вышло бы на любой машине. Да вы можете найти готовый бинарник для вашей системы и положить его рядом или внутрь проекта, но это бы означало уже достаточное колличество ручной подгонки, чего нам очень не хотелось. +Самым правильным вариантом оказался, это добавить необходимы рецепт (да они называют файл для сборки) в `conan-center-index`. Мы подготовили его и отправили на ревью мейнтейнерам конан индекса. +Если хотите, чтобы опыт использования шаблона был мягким и прятным, то побалуйте нас своим лайком на issue [libodb](https://github.com/conan-io/conan-center-index/pull/30476) и [libodb-pgsql](https://github.com/conan-io/conan-center-index/pull/30477) тем самым вы приблизите публикацию пакета в `conan-center-index`. + +### Второй камень +Как вы помните нам важны были миграции в используемой ORM. Вышло что многие ORM не поддерживают ее из коробки. Даже если и поддерживают, то делают это как-то наоборот, сначала должна быть ревизия в БД, после которой ORM составляют миграции на ее основе (очень странная логика). Так как `libodb` нам по большим параметрам подходил, оставалось только закрыть потребность в миграциях. +Именно поэтому миграции пришлось выносить в отдельный инструмент. В итоге для этой задачи был внедрён `Alembic` - официальный инструмент миграций для `SQLAlchemy`, предназначенный именно для управления версиями схемы и применения ревизий базы данных. + +Порядок действий такой: +- Ставим Python и пакеты alembicю psycopg2-binary и sqlalchemy-utils +- Инициализируем alembic командой `alembic init migrations` +- Описываем модель, только уже на питоне +- И запускаем генерацию миграции `alembic revision --autogenerate -m "new migration"` +- И накатываем ее в базу при необходимости `alembic upgrade head` + + +И в целом такой опыт встраивания инструментов абсолютно другого стека может означать, что C++ шаблон можно подружить с любым инструментом или фреймворком. Хотя, конечно, в данном случае, это не совсем прямая интеграция. + +### Третий камень +В этом шаблоне хотелось, не использовать CMake вовсе, либо свести его использование к минимуму, поэтому и был выбран conan. К сожалению, чем больше мы работали с Conan тем больше мы понимали что это заблуждение. Conan это именно пакетный менеджер, но никак не система сборки. В этом он все так же полагается на CMake/Meson и т.д. +Так что было принято решение свести к минимуму работу с CMake. \ No newline at end of file From 5e0e4ac92b49d58d7e4e75599df1b1834bdf4d26 Mon Sep 17 00:00:00 2001 From: Artem Sheptunov <106321977+Infindery@users.noreply.github.com> Date: Tue, 30 Jun 2026 09:14:09 +0500 Subject: [PATCH 04/19] docs: #71: update the first part of article --- docs/article.md | 44 ++++++++++++++------------------------------ 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/docs/article.md b/docs/article.md index b588b20..af2322e 100644 --- a/docs/article.md +++ b/docs/article.md @@ -29,54 +29,38 @@ # Взять и использовать или как мы упростили себе жизнь при разработке на C++ -Представьте, что вы решили начать разработку нового API С++ бекенда и вместо того, чтобы создавать новый репозиторий, искать подходящие инструменты и настраивать всю инфраструктуру с нуля, нажимаете пару кнопок и шаблон для вашего будущего API с настроенной инфраструктурой уже готов. +Представьте, что вы решили начать разработку нового API C++ бекенда и вместо того, чтобы создавать новый репозиторий, искать подходящие инструменты и настраивать всю инфраструктуру с нуля, нажимаете пару кнопок и шаблон для вашего будущего API с настроенной инфраструктурой уже готов. -Представили? Мы тоже и ограничиваться одним только представлением не стали, поэтому в серьез задумались об воплощении такого шаблона в жизнь. К тому же, как и всегда, всю разработку вели open-source, поэтому каждый из вас может просто "взять и использовать" этот шаблон для своих целей. И это не просто красивые слова - взять его можно [тут](https://github.com/TourmalineCore/to-dos-api-cpp), а использовать [так](https://github.com/TourmalineCore/to-dos-api-cpp/blob/master/README.md). +Представили? Мы тоже и ограничиваться одним только представлением не стали, поэтому всерьез задумались о воплощении такого шаблона в жизнь. К тому же, как и всегда, разработку вели в формате open-source, поэтому каждый из вас может просто [взять](https://github.com/TourmalineCore/to-dos-api-cpp) и [использовать](https://github.com/TourmalineCore/to-dos-api-cpp/blob/master/README.md) его для своих целей. -А мы дальше расскажем, как рождался этот шаблон, почему он устроен именно так, и через какие подводные камни нам пришлось пройти, чтобы каждый читатель мог взять этот шаблон и уверенно стартовать свой проект, не тратя время на настройку необходимой инфраструктуры и не наступая на те же грабли. +С его помощью вы получаете готовую инфраструктуру API C++ бекенда: настроенную систему сборки под разные конфигурации, менеджер пакетов, библиотеку тестирования, пайплайн и миграции - остается лишь склонировать и начать добавлять эндпоинты и модели. -< --- > +Как же появилась идея создания подобного шаблона? Ранее мы уже занимались разработкой чего-то подобного и в закромах своего GitHub имеем несколько шаблонов, которые вместе реализуют приложение для управления задачами - To-Dos. Кстати, один из них использовался в качестве примера в докладе нашего руководителя Александра, который проводил [воркшоп](https://techtrain.ru/talks/20004151/) на тему TDD разработки на фронтенде. -Всё начиналось с того, что нам потребовался шаблон API бэкенда на C++. Как известно, благодаря своей специфике, C++ предоставляет разработчику полный контроль над ресурсами, что открывает возможности для создания высоконагруженных API с тонкой оптимизацией, где важны скорость и эффективность работы. +Мы решили реализовать что-то похожее, но для C++, чтобы не тратить время на рутину, такую как настройку сборки, конфигурацию окружения и т.п., а быстро стартовать новый проект, подобно create-react-app, когда одной команды в терминале достаточно, чтобы перед тобой появилась готовая структура проекта со всем необходимым. -В закромах своего GitHub мы уже имеем несколько шаблонов на разных языках, которые вместе реализуют приложение для управления задачами - To-Dos. Кстати, один из них использовался в качестве примера в докладе нашего руководителя Александра, который проводил воркшоп на тему TDD разработки на фронтенде. +Наверное, каждый разработчик перед тем, как начать писать API backend на C++, задаётся вопросом - не делает ли он ошибку, выбирая такой язык, как C++, для реализации? Не будет ли правильнее взять что-то более современное и устоявшееся, как например .NET, JavaScript или Python? И если говорить начистоту, то в большинстве случаев сменить язык будет правильным выбором, но, несмотря на это, С++ имеет свои сильные стороны. -Мы решили реализовать что-то похожее но для C++, чтобы при необходимости не тратить время на одну и ту же рутину по типу настройки сборки или конфигурации окружения, а подобно create-react-app, когда одной команды в терминале достаточно, чтобы перед тобой появилась готовая структура проекта со всем необходимым, просто взять шаблон и сразу писать код. +Так, благодаря своей специфике, C++ предоставляет разработчику полный контроль над ресурсами и производительностью приложения, что открывает возможности для создания высоконагруженных API, где важны скорость и эффективность работы. Но не стоит забывать, что при этом приходится жертвовать скоростью разработки, поскольку сложность и вероятность совершить ошибку становятся существенно выше. -< --- > +Поэтому, если цель - это простой API с несложными выборками из базы данных, то C++, скорее всего, не ваш вариант. В этом случае можно присмотреться к готовым шаблонам на [.NET](https://github.com/TourmalineCore/to-dos-api-dot-net) или [NestJS](https://github.com/TourmalineCore/to-dos-api). Но если речь о проекте со сложной архитектурой, множеством интеграций и высокими требованиями к производительности, где важна тонкая настройка под конкретные сценарии, - это именно то, для чего C++ подходит лучше всего. -"Взять и использовать" звучит как "легко и просто" и по нашему мнению в современной разработке все должно следовать этому подходу, по крайней мере, нам, как разработчикам, этого бы хотелось. Но "легко и просто" разбивается о реальность, когда заходит речь о системе сборки и работы с зависимостями в C++. - -Так повелось, что в комьюнити C/C++ разработчиков не сформировалось единого подхода к сборке проекта и управлению зависимостями, в отличие от JavaScript, Python и многих других языков. Одни связывают это с возрастом языка, когда C только появился, самой идеи пакетного менеджера ещё не существовало. Другие, с тем, что C++ компилируется в нативный код, а значит одна и та же библиотека под разные платформы и архитектуры ведёт себя по-разному. Скорее всего, правы и те, и другие. +Ещё один плюс в пользу C++ в таких задачах - это результат компиляции. На выходе вы получаете самостоятельный бинарник, а не приложение, которому для запуска нужен установленный рантайм (вроде .NET runtime или Node.js) и набор зависимостей вокруг него. Благодаря этому не нужно следить, установлена на сервере нужная версия рантайма или нет, что уменьшает окно потенциальных ошибок и неопределенного поведения. Бинарник одинаково ведёт себя везде, куда его скопировали. -Но нас, как разработчиков, исторический экскурс интересует меньше, чем конкретный вопрос - как выстроить комфортный рабочий процесс в подобных агрессивных условиях? +Дальше расскажем, как шаблон устроен и почему он выглядит именно так, а также через какие подводные камни нам пришлось пройти, чтобы каждый, кто его возьмет, мог уверенно стартовать свой проект, не уделяя время настройке необходимой инфраструктуры и не наступая на те же грабли. +< --- Конец первой части --- > +"Взять и использовать" звучит как "легко и просто" и по нашему мнению в современной разработке все должно следовать этому подходу, по крайней мере, нам, как разработчикам, этого бы хотелось. Но "легко и просто" разбивается о реальность, когда заходит речь о системе сборки и работы с зависимостями в C++. -Наверное, у каждый разработчик перед тем, как начать писать API backend на C++, задаётся вопросом - а не делаю ли я ошибку, выбирая такой язык, как C++, для реализации подобного типа проекта? Не будет ли правильнее взять что-то более современное и устоявшееся, как например dotnet, JavaScript или Python? И если говорить на чистоту, то в большинстве случаев сменить язык будет правильным выбором, но, несмотря на это, С++ имеет свои сильные стороны. +Так повелось, что в комьюнити C/C++ разработчиков не сформировалось единого подхода к сборке проекта и управлению зависимостями, в отличие от JavaScript, Python и многих других языков. Одни связывают это с возрастом языка, когда C только появился, самой идеи пакетного менеджера ещё не существовало. Другие, с тем, что C++ компилируется в нативный код, а значит одна и та же библиотека под разные платформы и архитектуры ведёт себя по-разному. Скорее всего, правы и те, и другие. -Так, благодаря своей специфике, C++ предоставляет разработчику полный контроль над ресурсами и производительностью приложения, что открывает возможности для создания высоконагруженных API с тонкой оптимизацией, где важны скорость и эффективность работы. В результате можно добиться не только высокой производительности, но и качественного пользовательского опыта при взаимодействии с таким сервисом. +Но нас, как разработчиков, исторический экскурс интересует меньше, чем конкретный вопрос - как выстроить комфортный рабочий процесс в подобных агрессивных условиях? Взять и использовать - легко и просто, кажется, что в современной разработке все должно следовать этому подходу, по крайней мере, нам, как разработчикам, этого бы хотелось. Но что делать, когда история вносит в процесс свою директиву, особенно когда речь идёт о разработке на C++. Так повелось, что в комьюнити C/C++ разработчиков не сформировалось единого подхода к сборке проекта и работы с его зависимостями, как это произошло в JavaScript, Python и других языках. Некоторые говорят, что это связано с тем, что на момент появления языка, такого запроса не было, некоторые что то-то. Все эти утверждения носят правдивый характер, но как действовать нам, разработчикам, в условиях агрессивной среды, когда хочется наладить процесс нашей работы так, чтобы всё было легко и просто. именно этот слоган будет лежать в основе дальшейнего повествования. - - -В нашей компании мы создаём программные продукты, используя широкий спектр технологий и не ограничиваясь одним стеком. Сегодня в нашем арсенале уже есть .NET, JavaScript/TypeScript, С/C++, Python и множество инструментов - от React, Next.js и NestJS до .NET, Python, Kubernetes, kind и S3, и это лишь часть того, с чем мы работаем ежедневно. - -Но для нас важно не только применять уже полученные знания, но постоянно расширять горизонты: осваивать новые языки, пробовать новые инструменты и совершенствовать наши процессы. Одной из таких инициатив стало наше давнее желание попробовать C++ в бекенд разработке, использовав его для реализации шаблона API в виде приложения для управления задачами - To-dos. - -В нашем GitHub уже представлен пример такого приложения, которое было написано на TypeScript`е с использованием инструмента NestJS, поэтому задача казалась не такой сложной - нужно лишь повторить то, что уже сделано, но только на C++. К тому же, помимо API, у нас уже есть готовый UI, а также локальный environment, который мы называем local-env, в лице кластера kind. - -Кстати, наш руководитель Александр проводил воркшоп на тему TDD разработки на фронтенде, где в качестве примера использовался вышеупомянутый UI, а также выступал с докладом на <название конфы> про local-env. Очень рекомендуем ознакомиться. - -Может возникнуть вопрос, а зачем вообще пытаться реализовать backend на C++? Ведь кажется, что это не самый очевидный и популярный в сообществе язык для подобных целей и можно было бы использовать что-нибудь другое. - -Однако у C++ есть свои сильные стороны, так благодаря своей специфике он даёт полный контроль над ресурсами и производительностью приложения. Это открывает возможности для создания высоконагруженных API с тонкой оптимизацией, где важны скорость и эффективность работы. В результате можно добиться не только высокой производительности, но и качественного пользовательского опыта при взаимодействии с таким сервисом. - -Так вот, цель поставлена, пример есть, остаётся только выбрать инструменты и реализовать проект. - <---> Сперва мы решили не бросаться во все тяжкие, а сначала поискать уже существующие примеры - статьи, доклады, а может, и готовые проекты, которые могли бы ускорить наш старт и заранее подсветить возможные подводные камни. Однако довольно быстро стало понятно, что на поверхности информации крайне мало. Материалов по разработке backend`а на C++ оказалось заметно меньше, чем мы думали, а подобные open source проекты и вовсе редкость. From 319bc89b674cc835208a19983679ba7d6cf9d4d5 Mon Sep 17 00:00:00 2001 From: Artem Sheptunov <106321977+Infindery@users.noreply.github.com> Date: Tue, 30 Jun 2026 11:55:41 +0500 Subject: [PATCH 05/19] docs: #71: update the first part of article --- docs/article.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/article.md b/docs/article.md index 0e08a36..604f1e7 100644 --- a/docs/article.md +++ b/docs/article.md @@ -31,13 +31,13 @@ Представьте, что вы решили начать разработку нового API C++ бекенда и вместо того, чтобы создавать новый репозиторий, искать подходящие инструменты и настраивать всю инфраструктуру с нуля, нажимаете пару кнопок и шаблон для вашего будущего API с настроенной инфраструктурой уже готов. -Представили? Мы тоже и ограничиваться одним только представлением не стали, поэтому всерьез задумались о воплощении такого шаблона в жизнь. К тому же, как и всегда, разработку вели в формате open-source, поэтому каждый из вас может просто [взять](https://github.com/TourmalineCore/to-dos-api-cpp) и [использовать](https://github.com/TourmalineCore/to-dos-api-cpp/blob/master/README.md) его для своих целей. +Представили? Мы тоже и ограничиваться одним только представлением не стали, поэтому всерьез задумались о воплощении такого шаблона в жизнь. К тому же, разработку вели в формате open-source, поэтому каждый из вас может просто [взять](https://github.com/TourmalineCore/to-dos-api-cpp) и [использовать](https://github.com/TourmalineCore/to-dos-api-cpp/blob/master/README.md) его для своих целей. -С его помощью вы получаете готовую инфраструктуру API C++ бекенда: настроенную систему сборки под разные конфигурации, менеджер пакетов, библиотеку тестирования, пайплайн и миграции - остается лишь склонировать и начать добавлять эндпоинты и модели. +С его помощью вы получаете готовую инфраструктуру API C++ бекенда: настроенную систему сборки под разные конфигурации, менеджер пакетов, библиотеку тестирования, пайплайн и миграции - остается лишь склонировать и попробовать, вдруг это подойдет и для вас. -Как же появилась идея создания подобного шаблона? Ранее мы уже занимались разработкой чего-то подобного и в закромах своего GitHub имеем несколько шаблонов, которые вместе реализуют приложение для управления задачами - To-Dos. Кстати, один из них использовался в качестве примера в докладе нашего руководителя Александра, который проводил [воркшоп](https://techtrain.ru/talks/20004151/) на тему TDD разработки на фронтенде. +Но до этого давайте разберемся, как появилась идея создания подобного шаблона. Ранее мы уже занимались разработкой чего-то подобного и в закромах своего GitHub имеем несколько шаблонов, которые вместе реализуют приложение для управления задачами - To-Dos. Кстати, один из них использовался в качестве примера в докладе нашего руководителя Александра, который проводил [воркшоп](https://techtrain.ru/talks/20004151/) на тему TDD разработки на фронтенде. -Мы решили реализовать что-то похожее, но для C++, чтобы не тратить время на рутину, такую как настройку сборки, конфигурацию окружения и т.п., а быстро стартовать новый проект, подобно create-react-app, когда одной команды в терминале достаточно, чтобы перед тобой появилась готовая структура проекта со всем необходимым. +Мы решили реализовать что-то похожее, но для C++, чтобы в будущем не тратить время на рутину, такую как настройку сборки, конфигурацию окружения и т.п., а быстро стартовать проект, подобно create-react-app, когда одной команды в терминале достаточно, чтобы перед тобой появилась готовая структура проекта со всем необходимым. Наверное, каждый разработчик перед тем, как начать писать API backend на C++, задаётся вопросом - не делает ли он ошибку, выбирая такой язык, как C++, для реализации? Не будет ли правильнее взять что-то более современное и устоявшееся, как например .NET, JavaScript или Python? И если говорить начистоту, то в большинстве случаев сменить язык будет правильным выбором, но, несмотря на это, С++ имеет свои сильные стороны. @@ -45,7 +45,7 @@ Поэтому, если цель - это простой API с несложными выборками из базы данных, то C++, скорее всего, не ваш вариант. В этом случае можно присмотреться к готовым шаблонам на [.NET](https://github.com/TourmalineCore/to-dos-api-dot-net) или [NestJS](https://github.com/TourmalineCore/to-dos-api). Но если речь о проекте со сложной архитектурой, множеством интеграций и высокими требованиями к производительности, где важна тонкая настройка под конкретные сценарии, - это именно то, для чего C++ подходит лучше всего. -Ещё один плюс в пользу C++ в таких задачах - это результат компиляции. На выходе вы получаете самостоятельный бинарник, а не приложение, которому для запуска нужен установленный рантайм (вроде .NET runtime или Node.js) и набор зависимостей вокруг него. Благодаря этому не нужно следить, установлена на сервере нужная версия рантайма или нет, что уменьшает окно потенциальных ошибок и неопределенного поведения. Бинарник одинаково ведёт себя везде, куда его скопировали. +В отличие от большинства технологий для задач бекенда, плюсом в пользу C++ можно считать результат компиляции. На выходе вы получаете самостоятельный бинарник, а не приложение, которому для запуска нужен установленный рантайм (вроде .NET runtime или Node.js) и набор зависимостей вокруг него. Благодаря этому не нужно следить, установлена на сервере нужная версия рантайма или нет, что уменьшает окно потенциальных ошибок и неопределенного поведения. Бинарник требует минимальное количество настройки, а иногда вовсе не требует. Дальше расскажем, как шаблон устроен и почему он выглядит именно так, а также через какие подводные камни нам пришлось пройти, чтобы каждый, кто его возьмет, мог уверенно стартовать свой проект, не уделяя время настройке необходимой инфраструктуры и не наступая на те же грабли. From 515531191a28adf24510d900ff19243cf2d725bf Mon Sep 17 00:00:00 2001 From: Artem Sheptunov <106321977+Infindery@users.noreply.github.com> Date: Tue, 30 Jun 2026 13:04:05 +0500 Subject: [PATCH 06/19] docs: #71: change enumeration of languages to frameworks --- docs/article.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/article.md b/docs/article.md index 604f1e7..b6a08bc 100644 --- a/docs/article.md +++ b/docs/article.md @@ -39,7 +39,7 @@ Мы решили реализовать что-то похожее, но для C++, чтобы в будущем не тратить время на рутину, такую как настройку сборки, конфигурацию окружения и т.п., а быстро стартовать проект, подобно create-react-app, когда одной команды в терминале достаточно, чтобы перед тобой появилась готовая структура проекта со всем необходимым. -Наверное, каждый разработчик перед тем, как начать писать API backend на C++, задаётся вопросом - не делает ли он ошибку, выбирая такой язык, как C++, для реализации? Не будет ли правильнее взять что-то более современное и устоявшееся, как например .NET, JavaScript или Python? И если говорить начистоту, то в большинстве случаев сменить язык будет правильным выбором, но, несмотря на это, С++ имеет свои сильные стороны. +Наверное, каждый разработчик перед тем, как начать писать API backend на C++, задаётся вопросом - не делает ли он ошибку, выбирая такой язык, как C++, для реализации? Не будет ли правильнее взять что-то более современное и устоявшееся, как например .NET, NestJS или FastAPI? И если говорить начистоту, то в большинстве случаев сменить язык будет правильным выбором, но, несмотря на это, С++ имеет свои сильные стороны. Так, благодаря своей специфике, C++ предоставляет разработчику полный контроль над ресурсами и производительностью приложения, что открывает возможности для создания высоконагруженных API, где важны скорость и эффективность работы. Но не стоит забывать, что при этом приходится жертвовать скоростью разработки, поскольку сложность и вероятность совершить ошибку становятся существенно выше. From af4ff0ad3aa7b54778e53aaa0f767e6962fc856c Mon Sep 17 00:00:00 2001 From: Artem Sheptunov <106321977+Infindery@users.noreply.github.com> Date: Tue, 30 Jun 2026 16:55:35 +0500 Subject: [PATCH 07/19] docs: #71: add headers; add technologies section --- docs/article.md | 48 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/docs/article.md b/docs/article.md index b6a08bc..6880195 100644 --- a/docs/article.md +++ b/docs/article.md @@ -49,23 +49,33 @@ Дальше расскажем, как шаблон устроен и почему он выглядит именно так, а также через какие подводные камни нам пришлось пройти, чтобы каждый, кто его возьмет, мог уверенно стартовать свой проект, не уделяя время настройке необходимой инфраструктуры и не наступая на те же грабли. -< --- Конец первой части --- > +## Выбор инструментов -"Взять и использовать" звучит как "легко и просто" и по нашему мнению в современной разработке все должно следовать этому подходу, по крайней мере, нам, как разработчикам, этого бы хотелось. Но "легко и просто" разбивается о реальность, когда заходит речь о системе сборки и работы с зависимостями в C++. +Возникает вопрос выбора инструментов, которые будут использоваться для реализации, а здесь всё начинается с поиска и изучения того, что предлагает экосистема. -Так повелось, что в комьюнити C/C++ разработчиков не сформировалось единого подхода к сборке проекта и управлению зависимостями, в отличие от JavaScript, Python и многих других языков. Одни связывают это с возрастом языка, когда C только появился, самой идеи пакетного менеджера ещё не существовало. Другие, с тем, что C++ компилируется в нативный код, а значит одна и та же библиотека под разные платформы и архитектуры ведёт себя по-разному. Скорее всего, правы и те, и другие. +Прежде всего мы решили посмотреть, что уже есть на эту тему - статьи, доклады, возможно, готовые проекты, которые могли бы упростить выбор и заранее подсветить подводные камни. Но довольно быстро стало понятно, что материалов на поверхности крайне мало. Статей и докладов о разработке бекенда на C++ оказалось меньше, чем мы рассчитывали, а open-source шаблоны подобного рода и вовсе редкость. -Но нас, как разработчиков, исторический экскурс интересует меньше, чем конкретный вопрос - как выстроить комфортный рабочий процесс в подобных агрессивных условиях? +Это, впрочем, не выглядело поводом для расстройства, а скорее стало дополнительным аргументом в пользу того, что тема слабо освещена и наш шаблон, а также опыт его разработки могли оказаться действительно полезны сообществу. Тем более что мы изначально планировали делать проект открытым и делиться этим опытом с другими. И опыт этот, прежде всего, об одной идее: нам, как разработчикам, хочется, чтобы работа была устроена легко и просто - это именно то, к чему мы стремились с самого начала. -Взять и использовать - легко и просто, кажется, что в современной разработке все должно следовать этому подходу, по крайней мере, нам, как разработчикам, этого бы хотелось. Но что делать, когда история вносит в процесс свою директиву, особенно когда речь идёт о разработке на C++. Так повелось, что в комьюнити C/C++ разработчиков не сформировалось единого подхода к сборке проекта и работы с его зависимостями, как это произошло в JavaScript, Python и других языках. Некоторые говорят, что это связано с тем, что на момент появления языка, такого запроса не было, некоторые что то-то. Все эти утверждения носят правдивый характер, но как действовать нам, разработчикам, в условиях агрессивной среды, когда хочется наладить процесс нашей работы так, чтобы всё было легко и просто. +### Система сборки -именно этот слоган будет лежать в основе дальшейнего повествования. +Но озвученная идея разбивается о реальность, как только речь заходит о сборке проекта, а в C++ это важная часть любого проекта. В отличие от JavaScript, Python и многих других языков, в комьюнити C/C++ так и не сложилось единого подхода, но из большинства CMake стал ближе всего к негласному стандарту, поскольку он кроссплатформенный, поддерживается практически всеми IDE и компиляторами, а большинство современных библиотек уже поставляются с готовой CMake-конфигурацией, и выбор здесь напрашивался сам собой. -<---> +### Пакетный менеджер + +С пакетным менеджером история повторилась и всё не так однозначно, и в первую очередь мы выбирали между vcpkg и Conan. Остановились на Conan, поскольку он, в отличие от vcpkg, не ограничен связкой CMake и MSBuild, а одинаково хорошо работает с разными системами сборки и позволяет точно описывать окружение через профили - компилятор, архитектуру, ОС, тип сборки. Эта гибкость является для нас важным аспектом, ведь шаблон должен был одинаково собираться под разные архитектуры и операционные системы, а не быть заточен под одну конкретную конфигурацию. + +### Компилятор + +С выбором компилятора повторилась похожая логика. MSVC отпал почти сразу, поскольку, как и в случае с vcpkg, не хотелось привязываться к конкретной платформе или IDE, а MSVC по сути жёстко связан с экосистемой Windows и Visual Studio. -Сперва мы решили не бросаться во все тяжкие, а сначала поискать уже существующие примеры - статьи, доклады, а может, и готовые проекты, которые могли бы ускорить наш старт и заранее подсветить возможные подводные камни. Однако довольно быстро стало понятно, что на поверхности информации крайне мало. Материалов по разработке backend`а на C++ оказалось заметно меньше, чем мы думали, а подобные open source проекты и вовсе редкость. +В выбор между GCC и Clang решающую роль сыграла экосистема. Поскольку Clang поставляется в составе LLVM, который включает в себя и другие полезные инструменты, такие как clang-tidy и clang-format для статического анализа и форматирования кода, которые развиваются параллельно компилятору Clang и благодаря этому имеют хорошую совместимость. -Вместо того чтобы разочароваться, мы увидели в этом возможность, ведь стало понятно, что тема явно недостаточно освещена в сообществе, а значит, наша работа может принести реальную пользу. Тем более, что ещё на этапе идеи мы планировали делать проект открытым и делиться полученным опытом с другими. +### Веб-фреймворк + +Несомненно важным элементом инфраструктуры стал веб-фреймворк, то есть то, поверх чего строятся сами эндпоинты и бизнес-логика API. Здесь мы остановились на Drogon, поскольку у него низкий порог входа и понятный синтаксис. Из минусов стоит отметить документацию. Некоторые её разделы содержат битые ссылки на соседние страницы, из-за чего порой было сложно проследить логику повествования и найти нужный материал. + +Помимо Drogon, рассматривали Oat++ и userver. В случае Oat++, он показался нам более сложным и многословным по сравнению с Drogon. С userver ситуация другая, фреймворк отсутствует в `conan-center-index`, а его установка подразумевает самостоятельную сборку из репозитория разработчиков по их собственному шаблону, что заметно увеличивает порог входа по сравнению с обычной установкой пакета одной командой и не подходит нам. <---> @@ -204,17 +214,23 @@ Oat++ Предоставляет разработчику набор макро ### Первый камень -Наверное один из самых насущных подводных камней оказалось отсутсвие необходимых пакетов в `conan-center-index`. В ходе поэтапного выбора необходимых инструментов внутри шаблона мы наконец-то подошли к выбору ORM. И самой, на нащ взгляд, перспективной библиотеки ODB для работы с БД не оказалось в `conan-center-index`. + +Наверное один из самых насущных подводных камней оказалось отсутствие необходимых пакетов в `conan-center-index` (реестр пакетов Conan). В ходе поэтапного выбора необходимых инструментов внутри шаблона мы наконец-то подошли к выбору ORM. И самой, на наш взгляд, перспективной библиотекой ODB для работы с базой данных не оказалось в `conan-center-index`. + И как бы мы не хотели, но просто нажать и запустить шаблон не вышло бы на любой машине. Да вы можете найти готовый бинарник для вашей системы и положить его рядом или внутрь проекта, но это бы означало уже достаточное колличество ручной подгонки, чего нам очень не хотелось. -Самым правильным вариантом оказался, это добавить необходимы рецепт (да они называют файл для сборки) в `conan-center-index`. Мы подготовили его и отправили на ревью мейнтейнерам конан индекса. -Если хотите, чтобы опыт использования шаблона был мягким и прятным, то побалуйте нас своим лайком на issue [libodb](https://github.com/conan-io/conan-center-index/pull/30476) и [libodb-pgsql](https://github.com/conan-io/conan-center-index/pull/30477) тем самым вы приблизите публикацию пакета в `conan-center-index`. + +Самым правильным вариантом оказался, это добавить необходимы рецепт (так называют файл для сборки) в `conan-center-index`. Мы подготовили его и отправили на ревью мейнтейнерам конан индекса. + +Если хотите, чтобы опыт использования шаблона был мягким и приятным, то побалуйте нас своим лайком на issue [libodb](https://github.com/conan-io/conan-center-index/pull/30476) и [libodb-pgsql](https://github.com/conan-io/conan-center-index/pull/30477) тем самым вы приблизите публикацию пакета в `conan-center-index`. ### Второй камень -Как вы помните нам важны были миграции в используемой ORM. Вышло что многие ORM не поддерживают ее из коробки. Даже если и поддерживают, то делают это как-то наоборот, сначала должна быть ревизия в БД, после которой ORM составляют миграции на ее основе (очень странная логика). Так как `libodb` нам по большим параметрам подходил, оставалось только закрыть потребность в миграциях. -Именно поэтому миграции пришлось выносить в отдельный инструмент. В итоге для этой задачи был внедрён `Alembic` - официальный инструмент миграций для `SQLAlchemy`, предназначенный именно для управления версиями схемы и применения ревизий базы данных. + +Как вы помните, нам важны были миграции в используемой ORM. Вышло что многие ORM не поддерживают ее из коробки. Даже если и поддерживают, то делают это как-то наоборот, сначала должна быть ревизия в базе данных, после которой ORM составляют миграции на ее основе (очень странная логика). Так как `libodb` нам по большим параметрам подходил, оставалось только закрыть потребность в миграциях. + +Именно поэтому миграции пришлось выносить в отдельный инструмент. В итоге для этой задачи был внедрён `Alembic` - официальный инструмент для `SQLAlchemy`, предназначенный именно для управления версиями схемы и применения миграций в базу данных. Порядок действий такой: -- Ставим Python и пакеты alembicю psycopg2-binary и sqlalchemy-utils +- Ставим Python и пакеты alembic, psycopg2-binary и sqlalchemy-utils - Инициализируем alembic командой `alembic init migrations` - Описываем модель, только уже на питоне - И запускаем генерацию миграции `alembic revision --autogenerate -m "new migration"` @@ -224,5 +240,7 @@ Oat++ Предоставляет разработчику набор макро И в целом такой опыт встраивания инструментов абсолютно другого стека может означать, что C++ шаблон можно подружить с любым инструментом или фреймворком. Хотя, конечно, в данном случае, это не совсем прямая интеграция. ### Третий камень + В этом шаблоне хотелось, не использовать CMake вовсе, либо свести его использование к минимуму, поэтому и был выбран conan. К сожалению, чем больше мы работали с Conan тем больше мы понимали что это заблуждение. Conan это именно пакетный менеджер, но никак не система сборки. В этом он все так же полагается на CMake/Meson и т.д. + Так что было принято решение свести к минимуму работу с CMake. \ No newline at end of file From ef85fb3603a06a3c42f5c75c8271f850d8a24bfb Mon Sep 17 00:00:00 2001 From: Oleg Kl Date: Tue, 30 Jun 2026 19:53:59 +0500 Subject: [PATCH 08/19] docs: #71: add more to issues chapter --- docs/article.md | 33 ++++++++++++++++++++++++++++++++- docs/conan_lib_connection.png | Bin 0 -> 14019 bytes 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 docs/conan_lib_connection.png diff --git a/docs/article.md b/docs/article.md index 6880195..a4c395c 100644 --- a/docs/article.md +++ b/docs/article.md @@ -243,4 +243,35 @@ Oat++ Предоставляет разработчику набор макро В этом шаблоне хотелось, не использовать CMake вовсе, либо свести его использование к минимуму, поэтому и был выбран conan. К сожалению, чем больше мы работали с Conan тем больше мы понимали что это заблуждение. Conan это именно пакетный менеджер, но никак не система сборки. В этом он все так же полагается на CMake/Meson и т.д. -Так что было принято решение свести к минимуму работу с CMake. \ No newline at end of file +Так что было принято решение свести к минимуму работу с CMake. Для этого у Conan в функции generate есть возможность подготовить всё необходимое для дальнейшей работы CMake. В нашем случае мы использовали CMakeDeps и CMakeToolchain. Первый отвечает за генерацию файлов зависимостей, чтобы CMake мог корректно находить подключённые библиотеки, а второй за генерацию toolchain-файла с нужными параметрами сборки. + +```python +# Использование генераторов +def generate(self): + deps = CMakeDeps(self) + deps.generate() + + tc = CMakeToolchain(self, generator="Ninja") + tc.generate() +``` + +Оставшаяся ответственность для `CMakeLists.txt` оставалось только подключение библиотек из Conan. Для этого просто необходимо добавить несколько строк в `CMakeLists.txt`, которые можно взять со страницы любого пакета из [`conan-center-index`](https://conan.io/center/recipes/drogon?version=1.9.13) + +![Подключение пакетов в CMakeLists](conan_lib_connection.png) + +### Четвертый камень + +Когда мы принялись за сборку проекта на несколько платформ с разными архитектурами, то столкнулись с проблемой запуска тестов. Да тестов шаблоне можно сказать, что нету. Но они настроены и готовы к запуску, осталось их только написать. + +И при разработке с хостовой машины для хостового таргета, проблем нету. Проект собирается и тесты запускаются. А как только дело касается сборки проекта кросс-платформенно, то проект может и соберется через кросс-платформенные компиляторы, но тесты уже не запустятся, так как *сюрприз* они собраны для запуска на другой платформе. + +Думаем многие уже знают как решить эту проблему: запустить сборку в Github Actions без кросс-платформы, а сразу использовать таргетную машину и запускать тесты. + +В целом так оно и есть и оно работает. Но с какой скоростью это уже другой вопрос. + +### Пятый камень + +Скорость сборки при это существенно падала. При сборке локально, было приемлемо подождать один раз, пока Conan соберет исходники всех подключаемых библиотек и соберет их конкретно под машину, а потом использовать эти артефакты собрки дальше, без необходимости их пересобирать. А вот при сборке в Github Actions это происходит каждый запуск. И на сборку хотя бы двух архитектур (arm64 и x86-64) только на linux уходило около 30 минут. + +Немного подумав пришли к решению, которое нас устравало в локальной разработке - хранить артефакты сборки библиотек от Conan. Для этого в Github Actions кешировались зависимости сборки из папки `conan2/` и использовались при каждом последующем запуске. + diff --git a/docs/conan_lib_connection.png b/docs/conan_lib_connection.png new file mode 100644 index 0000000000000000000000000000000000000000..81e1d57c41b1df3cecec5bc4282c3a95039e03a2 GIT binary patch literal 14019 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 Wq%Huwh(Deo$w@1HER!_;_CEmIqL_35 literal 0 HcmV?d00001 From 49fbe107a144bdb9b7024a0ee7457ba6367a910e Mon Sep 17 00:00:00 2001 From: JDRkow Date: Thu, 2 Jul 2026 00:55:50 +0300 Subject: [PATCH 09/19] docs: #71: add internal template solutions chapter --- docs/article.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/article.md b/docs/article.md index a4c395c..41ee677 100644 --- a/docs/article.md +++ b/docs/article.md @@ -210,6 +210,45 @@ Oat++ Предоставляет разработчику набор макро В процессе поиска мы наткнулись на несколько иснтрументов - ODB и TinyORM. Если говорить о TinyORM, то он предоставляет средства управления типами связей и синтаксис запросов в стиле LINQ. + + +Хочется обратить внимание на некоторые тонкости, которые были внутри шаблона. + +Одной из улучшений было заимстованно с conan. Начнем с профилей. У Conan есть понятие [профилей](https://docs.conan.io/2/reference/config_files/profiles.html). Это по сути краткий свод правил в которые входит: какой компилятор использовать, флаги компилятора, версия C++ и многое другое. В Conan в документации почти каждый раз перед сборкой проекта рекомендует запустить `conan profile detect`, что определит профиль по вашей системе. Но так как мы хотели использовать clang в качестве компилятора с 20 версией C++, то появилась мысль написать свой профиль, так как по умолчанию Conan всегда находил gcc компилятор с какой-нибудь старой версией C++. + +``` +[settings] +# Определяет систему автоматически через detect_api (например Linux, Windows, Macos) +os={{detect_api.detect_os()}} +# Определяет архитектуру автоматически через detect_api (например x86_64, armv8) +arch={{detect_api.detect_arch()}} +# Тип сборки +build_type=Debug +# Компилятор который будет использоваться +compiler=clang +# Версия компилятора Clang +compiler.version=14 +# Реализация стандартной библиотеки C++ +compiler.libcxx=libstdc++11 +# Стандарт C++ +compiler.cppstd=gnu20 + +[conf] +# Явно указывает, какие исполняемые файлы использовать для C и C++ компиляции +tools.build:compiler_executables={"c":"clang","cpp":"clang++"} +# Задаёт количество параллельных job равное числу доступных ядер процессора +tools.build:jobs={{os.cpu_count()}} +# Разрешает системному пакетному менеджеру (apt, brew и т.д.) автоматически устанавливать недостающие системные зависимости +tools.system.package_manager:mode=install +``` + +Тем самым мы жестко зафиксировали некоторые параметры сборки, но так же оставили гибкость профиля для работы на других системахю. Это потом помогло нам переиспользовать этот профиль так же в Github Actions, а не определять его там атоматически. + +Единственное, что необходимо сделать с профилем, это положить его в папку `/root/.conan2/profiles/default`. + +Далее во время использования ODB должны были каждый раз генерировать вспомогательные файлы моделей, для дальнейшей работы ODB runtime. + +Так как это действие происходило достаточно редко, только во время создания модели или ее редактирование, постоянно держать эту команду в уме становилось тяжко, а так же это буквально единственное применение CLI инструмента ODB, то мы вынесли ее использование в скрипт `scripts/generate_odb_files.sh`. From 6786133b3600a53293c1ba4a7efdf7d80b29655e Mon Sep 17 00:00:00 2001 From: Artem Sheptunov <106321977+Infindery@users.noreply.github.com> Date: Thu, 2 Jul 2026 10:19:15 +0500 Subject: [PATCH 10/19] docs: #71: remove redundant content; add internal structure section --- docs/article.md | 171 +++++++++++++++++++----------------------------- 1 file changed, 68 insertions(+), 103 deletions(-) diff --git a/docs/article.md b/docs/article.md index 41ee677..76ff30b 100644 --- a/docs/article.md +++ b/docs/article.md @@ -67,150 +67,115 @@ ### Компилятор -С выбором компилятора повторилась похожая логика. MSVC отпал почти сразу, поскольку, как и в случае с vcpkg, не хотелось привязываться к конкретной платформе или IDE, а MSVC по сути жёстко связан с экосистемой Windows и Visual Studio. +С компилятором повторилась похожая логика. MSVC отпал почти сразу, поскольку, как и в случае с vcpkg, не хотелось привязываться к конкретной платформе или IDE, а MSVC по сути жёстко связан с экосистемой Windows и Visual Studio. -В выбор между GCC и Clang решающую роль сыграла экосистема. Поскольку Clang поставляется в составе LLVM, который включает в себя и другие полезные инструменты, такие как clang-tidy и clang-format для статического анализа и форматирования кода, которые развиваются параллельно компилятору Clang и благодаря этому имеют хорошую совместимость. +В выборе между GCC и Clang решающую роль сыграла экосистема. Clang поставляется в составе LLVM, который включает в себя и другие полезные инструменты, такие как clang-tidy и clang-format для статического анализа и форматирования кода, которые развиваются параллельно компилятору Clang и благодаря этому имеют хорошую совместимость, поэтому мы решили использовать его. ### Веб-фреймворк -Несомненно важным элементом инфраструктуры стал веб-фреймворк, то есть то, поверх чего строятся сами эндпоинты и бизнес-логика API. Здесь мы остановились на Drogon, поскольку у него низкий порог входа и понятный синтаксис. Из минусов стоит отметить документацию. Некоторые её разделы содержат битые ссылки на соседние страницы, из-за чего порой было сложно проследить логику повествования и найти нужный материал. +В выборе веб-фремворка мы остановились на Drogon, поскольку, как нам показалось, у него низкий порог входа и понятный синтаксис. Из минусов можем отметить документацию, поскольку некоторые разделы содержат битые ссылки, из-за чего порой было сложно проследить логику и найти нужный материал. -Помимо Drogon, рассматривали Oat++ и userver. В случае Oat++, он показался нам более сложным и многословным по сравнению с Drogon. С userver ситуация другая, фреймворк отсутствует в `conan-center-index`, а его установка подразумевает самостоятельную сборку из репозитория разработчиков по их собственному шаблону, что заметно увеличивает порог входа по сравнению с обычной установкой пакета одной командой и не подходит нам. +Помимо Drogon, рассматривали Oat++ и userver. В случае Oat++, он показался нам более сложным и многословным по сравнению с Drogon. С userver ситуация другая, фреймворк отсутствует в `conan-center-index`, а его установка подразумевает самостоятельную сборку из репозитория разработчиков по их собственному шаблону, что не подходит нам. -<---> +### ORM -Как мы уже упоминали, опыт работы с C/C++ у нас был и раньше. Однако при использовании сторонних библиотек мы придерживались довольно консервативного подхода. Скачать исходники, собрать проект под нужную конфигурацию, слинковать с собственным бинарником, повторить шаг первый... Думаем, многим это знакомо. +В качестве ORM системы изначально мы рассматривали те, что встроенны в фреймворки Drogon и Oat++, но в последствии отказались от этой идеи, поскольку они ориентированы на работу в формате SQL first, а не следуя паттерну Data Mapper, как нам хотелось. Кроме того, Drogon подразумевает, что база данных создаётся и наполняется таблицами вручную через SQL, и только после этого утилита drogon_ctl подключается к базе и генерирует C++ классы моделей по уже существующей схеме. -Но, как это часто бывает, начинаешь по-настоящему ценить удобство, только попробовав альтернативы. Поработав с другими языками и их экосистемами, где управление зависимостями давно упрощено с помощью пакетных менеджеров, мы невольно начали скучать по такому же уровню комфорта. Возвращаясь к C++, мы задались вполне логичным вопросом - а есть ли здесь что-то сопоставимое с npm, pip или cargo? +В итоге мы остановились на ODB, поскольку, на наш взгляд, это более продвинутый инструмент, который позволяет работать с базой данных через объекты и их методы, а не писать SQL напрямую. Стоит отдельно упомянуть про лицензию, ODB распространяется под GPLv3, что подразумевает открытый исходный код. Code Synthesis, разработчик ODB, также предлагает бесплатную проприетарную лицензию (FPL) для небольших проектов, её можно получить, если объём сгенерированного кода поддержки базы данных в рамках одного релиза приложения не превышает 10 000 строк (~10-20 классов моделей). Для более крупных закрытых проектов потребуется приобрести коммерческую проприетарную лицензию (CPL). -И тут мы столкнулись с особенностью, исторически для C/C++ так и не сформировалось единого, общепринятого пакетного менеджера, аналогичного тем, что существуют в других языках. Кто-то использует vcpkg, кто-то Conan, а кто-то продолжает повторять шаг первый, второй и третий (то, как действовали мы, упоминая об этом ранее). + -Мы решили попробовать внедрить один из существующих пакетных менеджеров в наш проект, поэтому сформировали несколько требований для его выбора, а именно: -- **Простота в использовании**. - + Установка пакетного менеджера и первоначальная конфигурация должна быть простой. Кроме того, в своей работе мы активно используем VSCode DevContainers, поэтому интеграция с подобной средой должна была быть возможна. - + Команды и общий workflow должны быть интуитивно понятными, чтобы не повышать порог входа в проект. - + Пакетный менеджер должен иметь обширную документацию, желательно с примерами. -- **Пакетная экосистема**. - + В библиотеке менеджера должно быть представлено достаточное количество пакетов, чтобы можно было вести разработку. - + Библиотека менеджера должна содержать пакеты для актуальных инструментов (такие как, веб-фреймворки, ORM и тп.). -- **Интеграция с системами сборки**. - + Поддержка CMake, Make и других систем сборки, а также поддержка сборки внутри docker-контейнера. - + Простая интеграция без необходимости изменять существующие скрипты сборки. - + Генерация конфигурационных файлов. -- **Скорость работы**. - + Быстрая загрузка и установка зависимостей (имеет значение при пересборке контейнера). +### Миграции -Определившись с требованиями, мы начали искать существующее решение, удалетворяющее озвученным требованиям. Почти сразу на наш стол попали vcpkg и Conan, которые, как нам в тот момент показалось, пользовались большей популярностью в комьюнити разработчиков. Каждый из них в той или иной мере удолетворял нашим требованиям, но также и имел свои особенности. +В момент реализации механизма применения миграций к базе данных мы еще не были знакомы с возможностями ODB в этой области. Как оказалось, с версии 2.3.0 ODB добавил поддержку ревизий схемы базы данных, включая генерацию миграций на основе изменений в моделях. Тогда же мы решили использовать отдельный инструмент - Alembic. Кроме того, мы увидели возможность показать в шаблоне пример того, как C++ API может взаимодействовать с инструментами из другого окружения, в нашем случае Python. -Так, оба инструмента были в равной степени просты в установке, имели интуитивно понятный набор команд, обладали исчерпывающий документацией и большим объемом библиотеки пакетов. Различия начались с организации хранения пакетов. Тогда, как Conan обладал децентрализованной организацией хранения, что позволяет хранить пакеты в удалённом индексе или локально, vcpkg имел организацию централизованную на удалённом индексе, принадлежащим Microsoft. +Alembic показался нам хорошим выбором, поскольку реализовать логику применения миграций и описать модели с его помощью оказалось довольно просто. Но стоит отметить, что для этого пришлось завести отдельное представление модели на Python, которое повторяло структуру моделей ODB. -Мы составили таблицу, в которой провели сравнение этих инструментов и вот, что у нас получилось: +## Внутреннее устройство шаблона -| Критерий | **Conan** | **vcpkg** | -|-------------------------------------|------------------------------------|----------------------------------| -| Простота установки | + | + | -| Интуитивно понятный | + | + | -| Обширная документация | + | + | -| Объём библиотеки пакетов | + | + | -| Хранение пакетов | + (децентрализованное) | + (централизованное) | -| Интеграция с системами сборки | + (любая) | + (CMake, MSBuild) | -| Простота интеграции | + (без принудительного toolchain) | - (обязательный CMake toolchain) | -| Поддержка бинарных пакетов | + (CI/CD, пресборка devcontainer) | + (менее гибко) | -| Поддержка пакетов с исходным кодом | + (очень гибко) | + (ограниченно) | +Как всё это выглядит на практике? Начнём с общей структуры, а дальше подробнее остановимся на отдельных её частях. -Поскольку изначально мы рассматривали этот проект как шаблон для последующей разработки на C++, для нас было важно выбирать максимально универсальные инструменты. +### Структура проекта -Не секрет, что vcpkg развивается при поддержке Microsoft и, несмотря на свою кроссплатформенность, лучше всего раскрывается при работе на Windows, особенно в связке с Visual Studio. В других окружениях его использование также возможно, но нередко требует дополнительной настройки. - -Кроме того, vcpkg в ориентирует на использование CMake toolchain или интеграцию с MSBuild через Visual Studio. Несмотря на то, что мы также рассматривали CMake в качестве основной системы сборки, возможность Conan работать с разными системами показалась нам более универсальным решением. - -Как вы возможно уже догадались, наш выбор пал на Conan. Именно его мы встроили и использовали в нашем проекте. - -Блягодаря внедрению Conan мы смогли упростить наш опыт работы с зависимостями проекта, теперь установка и сборка происходила под капотом, а нам оставалось лишь указать нужную зависимость в конфигурационном файле и запустить процесс. - -Проблема была решена и тогда мы решили, что сложностей больше не должно возникать, по крайней мере нам так казалось на тот момент, но об этом позднее. - -<---> - -Следующее, что нам нужно было сделать - выбрать компилятор, который бы мы использовали в проекте. В моменте сразу же промелькнула мысль об использовании компиляторов GCC или Clang, MSVC мы не рассматривали, поскольку не хотели привязываться к определённой платформе или IDE, как упоминали ранее. - -Изначально нам казалось, что для наших задач оптимальным выбором будет GCC, а Clang воспринимался скорее как инструмент из экосистемы Apple. Однако, углубившись в вопрос кроссплатформенной сборки, мы пересмотрели этот взгляд. +```toml +- .devcontainer/ # Конфигурация VSCode DevContainer +- .github/ # Конфигурация пайплайна GitHub Actions +- .vscode/ # Настройки редактора и сниппеты кода для VSCode +- alembic/ # Конфигурация Alembic и история миграций +- ci/ # Конфигурация для запуска API в кластере k8s +- deps/ # Собственные Conan-рецепты зависимостей +- docs/ # Документация проекта +- profiles/ # Conan-профили под отдельные конфигурации сборки +- scripts/ # Вспомогательные скрипты +- src/ # Код приложения +- test/ # Unit и e2e тесты +- test_package/ # Служебный conanfile для проверки собранного Conan-пакета +- .clang-format # Конфигурация автоформатирования кода +- .clang-tidy # Конфигурация статического анализа кода +- .env.example # Пример файла переменных окружения +- .gitattributes # Настройки Git +- .gitignore # Список файлов и директорий, игнорируемых Git +- CMakeLists.txt # Корневой конфигурационный файл сборки +- CMakeUserPresets.json # Пресеты CMake, генерируемые Conan +- conanfile.py # Рецепт Conan-пакета API +- docker-compose.yml # Конфигурация для запуска базы данных и других сервисов +- Dockerfile # Многоступенчатая сборка продуктового образа +- LICENSE # Лицензия +- Makefile # Таргеты Makefile для быстрых команд +- README.md # Инструкции по разработке, сборке и запуску проекта +``` -Оказалось, что Clang в ряде сценариев может быть более удобным выбором, так при использовании GCC для кросс-компиляции зачастую требуется установка отдельных компиляторов под каждую целевую архитектуру, тогда как в случае с Clang многое доступно «из коробки» и достаточно лишь настроить линковщик и стандартные библиотеки. + -Кроме этого, Clang поставляется в составе LLVM, который включает в себя и другие полезные инструменты, например, clang-tidy для статического анализа кода и clang-format для автоматического форматирования, которые мы также смогли использовать в рамках нашего проекта. + + + -Для того, чтобы Conan использовал нужный компилятор, с целевой версией стандарта C++, а также собирал под нужную архитектуру с использованием правильного линковщика и статических библиотек, необходимо явно сказать ему об этом. Для подобных целей сущестуют профили Conan. Для наших целей мы опредлили в проекте 3 профиля, где указали нужные конфигурации для каждой из целевых платформ. Так у нас получился профиль для архитектуры amd64, для архитектуры arm64 и универсальный для разработки, в котором мы использовали возможности шаблонизатора Jinja2. Шаблонизатор позволил нам автоматически опредлять архитуктуру и платформу, что упрощает общий опыт разработки, особенно внутри devcontainer. +### Devcontainer -Важно учитывать, что для кроссплатформенной работы Conan необходимо явно определить параметры сборки, такие как используемый компилятор, целевую версию стандарта C++, архитектуру, линковщик, тип подключаемых библиотек и т.п. Для этого в Conan используюется механизм профилей. +Директория `.devcontainer` содержит конфигурацию для разработки внутри Dev Container в VS Code. Сборка самого контейнера идёт на основе `Dockerfile`, который лежит в той же директории и устанавливает весь набор инструментов, таких как Clang, CMake, Conan и остальные. Там же, в `Dockerfile`, лежащий рядом Conan-профиль `to-dos-conan-profile.conf` копируется в `/root/.conan2/profiles/default`, тем самым профиль устанавливается, как профиль по умолчанию, в результате Conan подхватывает его автоматически при любой сборке, без необходимости указывать профиль вручную через флаг. - -В рамках проекта мы определили три профиля с нужными конфигурациями под разные сценарии. Сейчас у нас есть отдельные профили для архитектур amd64 и arm64, а также универсальный профиль для разработки. В последнем мы использовали возможности шаблонизатора Jinja2, что позволило автоматически определять архитектуру и платформу, что упростило процесс разработки, особенно при работе внутри VSCode DevContainer. +Чтобы не собирать зависимости и проект с нуля при каждом пересоздании контейнера, в `devcontainer.json` настроено кэширование через именованные Docker volumes. Отдельно кэшируется директория `.conan2` со скачанными и собранными Conan-пакетами и директория сборки `build`, где хранятся сгенерированные Conan файлы toolchain'а для CMake. Без этого кэширования пересборка контейнера с нуля означала и пересборку всех зависимостей заново, а это ощутимо по времени (в нашем случае сборка занимала 20+ минут). -<---> +В том же `devcontainer.json` через хук `postCreateCommand` сразу после создания контейнера автоматически подключается локальное хранилище рецептов Conan, чтобы не приходилось выполнять эту команду вручную. -К веб-фреймворку у нас было не так много требований, как к пакетному менеджеру, но большинство являлось очень важными. +Также в конфигурации подключена [devcontainer feature](https://containers.dev/features) `docker-outside-of-docker`, которая позволяет работать с Docker на хост-машине изнутри devcontainer, а не поднимать вложенный Docker внутри контейнера. Кроме этого, использование позволяет наблюдать запущенные контейнеры через Docker Desktop, что достаточно удобно. -Так мы хотели найти решение, которое бы удолетворяло следующим требованиям: -- **Простота в использовании** - + Установка фреймворка и его настройка должна быть простой. - + Чистый и понятный синтаксис. - + Простая работа с query-параметрами, заголовкам и телом запроса. - + Обширная документация с примерами. -- **Возможности** - + Реализация middlewares в том числе для аунтификации. - + Работа в мультипоточном режиме, паралелльная обработка запросов. -- **ORM** - + Наличие собственной системы ORM, избавляющей от необходимости работать напрямую с SQL. - + Возможность реализации функционала работы с миграциями базы данных. +Чтобы контейнеры, поднятые на хосте, были доступны и по сети из самого devcontainer, в `runArgs` явно указан флаг `--network=host`, который подключает devcontainer к сетевому пространству хост-машины напрямую, а не изолирует его в собственной Docker-сети. -Мы выбрали несколько вариантов - userver, Oat++ и Drogon, которые нам показались более актуальными. +### Пакетные рецепты -В ходе работы мы поочерёдно попытались внедрить каждый из них в проект по отдельности и реализовывали минимальный набор функциональности для того, чтобы оценить опыт использования каждого, а также понять, насколько каждый из фреймворков соответствует нашим требованиям. +Как упоминали ранее, мы используем Conan в качестве пакетного менеджера. По умолчанию, устанавливая зависимость, Conan ищет её рецепт в собственном публичном индексе, который называется `conan-center-index`. Бывает так, что это хранилище не содержит нужного пакета, поэтому Conan предусмотрел возможность подключения собственных источников рецептов, в дополнение к официальному индексу. -Результирующая таблица сравнения, которая нас получилось: +Чтобы Conan смог увидеть локальные рецепты, директория, в которой находятся рецепты (в нашем случае `deps`), подключается как дополнительный локальный remote командой `conan remote add local-recipes ./deps --type=local-recipes-index`. После этого при установке зависимостей Conan берёт рецепты уже не из публичного индекса, а из локальной папки, собирает исходники под нужную конфигурацию и кладёт готовые бинарники в кэш, то есть точно так же, как если бы эти пакеты были частью conan-center-index. -| Критерий | Drogon | userver | Oat++ | -| ---------------------------| ----------------- | --------------------------- | --------------------- | -| Простота установки | + | - | + | -| Читаемый синтаксис | + | - | + (но сложнее Drogon) | -| Простая работа с запросами | + | (исключили из эксперимента) | + | -| Обширная документация | + (но с минусами) | (исключили из эксперимента) | + | -| Работа с middleware | + | (исключили из эксперимента) | + | -| Мультипоточная работа | + | (исключили из эксперимента) | + | -| Наличие high-level ORM | - | (исключили из эксперимента) | - | -| Возможность миграций | - | (исключили из эксперимента) | - | +### Тесты -На определённый момент мы столкнулись с тем, что установка userver оказалась довольно сложной, официальная документация в основном ориентировала нас на копирование шаблонного репозитория перед созданием собственного проекта. Кроме того, нам показалось, что синтаксис userver достаточно сложный. В итоге мы решили исключить его из дальнейших экспериментов. +В проекте реализовано два вида тестов: unit и E2E. - -В случае Drogon и Oat++ установка оказалась заметно проще, поскольку оба пакета были представлены в conan-center-index (хранилище пакетов Conan), поэтому мы решили продолжить проводить эксперименты только с ними. +Unit-тесты используют библиотеку GTest и вынесены в отдельную директорию `test` со своим `CMakeLists.txt`, в котором описана вся логика сборки тестового бинарника, он собирается как отдельный исполняемый файл, независимый от основного. Сборка тестов подключается в корневом `CMakeLists.txt` и может быть отключена переменной окружения `EXCLUDE_UNIT_TESTS_FROM_BUILD`. Также, сборка тестов не происходит при кросс-платформенной сборке, поскольку в этом случае не получится запустить сам исполняемый файл тестов на сборочной машине. - -Оба фреймворка, на наш взгляд, обладали простым и понятным синтаксисом, однако в Drogon он выглядел более прозрачным, так как не был обёрнут в кодовые макросы, также оба позволяли удобно работать с запросами и middlewares. +> На данный момент в директории лежит только один unit-тест, который служит примером. -Drogon, как и Oat++, имеет обширную документацию, однако иногда встречаются ссылки на разделы, которых по непонятным причинам нет в актуальной версии документации. Это порой приводит к затруднениям. +E2E-тесты реализованны с использованием библиотеки Karate. В случае E2E, вместо изолированного бинарника с тестами Karate выполняет HTTP-запросы к уже поднятому и работающему экземпляру API. Сценарий (тест) лежит в директории `e2e`, в файле `to-dos-happy-path.feature`, и последовательно проходит через все ключевые операции, начиная от создания задачи, и заканчивая удалением и финальной проверкой, что задачи больше нет в базе. - -Если говорить о мультипоточной работе, то оба фреймворка позволяют использовать несколько потоков и обрабатывать запросы параллельно. +Поскольку Karate запускается не как часть сборки C++ проекта, а как самостоятельный процесс, для него в той же директории `e2e` заведён отдельный `KarateDockerfile`, который использует за основу лёгкий образ с Java и скачанным `karate.jar`, не связанный с основным `Dockerfile` приложения. Такое разделение позволяет запускать e2e-тесты в двух разных окружениях: локально в `docker-compose` и в кластере k8s, где собранный образ приложения разворачивается через Helm. -Также для нас было важно, чтобы веб-фреймворк имел богатую методами собственную high-level ORM, чтобы избежать необходимости написания прямых SQL запросов, а также чтобы фреймворк поддерживал работу с миграциями базы данных. Кроме того, поскольку в своих проектах мы используем PostgreSQL, ORM также должна быть совместима с ней. +### CI-пайплайн -И Drogon, и Oat++ имеют собственный ORM фреймворк. +Пайплайн состоит из двух воркфлоу, расположенных в директории `.github/workflows`. -Oat++ Предоставляет разработчику набор макросов для работы с базой данных и поразумевает написание разработчиком собственных SQL-запросов. +Первый, `.reusable-docker-build-and-push.yml`, отвечает за сборку и публикацию Docker-образа API. Он выполняется в двух случаях: при пуше в `master`, тогда собирается и публикуется актуальный образ с тегом `latest`, либо по вызову из другого воркфлоу. Сама сборка идёт для двух архитектур, arm64 и amd64, затем оба образа объединяются в единый мультиархитектурный образ с помощью docker buildx. -В случае Drogon, фреймворк предоставляет набор методов для выполнения базовых команд покрывающих операции CRUD, но для более сложных задач также ориентирует на написание собственных SQL-запросов. Также, подразумевается генерация классов моделей по предварительно подготовленной базе данных, то есть это означает, что база данных и таблицы должны быть предварительно созданы разработчиком и только после этого drogon_ctl сможет сгенерировать классы моделей. +Второй воркфлоу, `e2e-tests-on-pull-request.yml`, запускается на каждый pull request и выполняет E2E-тесты. Выполнение тестов происходит в двух джобах, каждая из которых отвечает за собственное окружение. Первая джоба вызывает воркфлоу сборки образа API, дожидается публикации образа под конкретным коммитом и разворачивает его через Helm в тестовый кластер k8s, поднятый прямо в CI. Вторая джоба поднимает `docker-compose`, который собирает актуальный образ API и запускает его вместе с базой данных и контейнером Karate в общей docker-сети. - -Также, оба фреймворка не поддерживают работу с миграциями базы данных. Всё это подтолкнуло нас к поиску сторонних инструментов для решения этой проблемы. +### Alembic и миграции -<---> +### Код приложения -В процессе поиска мы наткнулись на несколько иснтрументов - ODB и TinyORM. +### Makefile -Если говорить о TinyORM, то он предоставляет средства управления типами связей и синтаксис запросов в стиле LINQ. - +### Профили и скрипты Хочется обратить внимание на некоторые тонкости, которые были внутри шаблона. From 3f5e4536fcba8a04c668b74a65274b800e415911 Mon Sep 17 00:00:00 2001 From: Artem Sheptunov <106321977+Infindery@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:49:01 +0500 Subject: [PATCH 11/19] docs: #71: update ORM paragraph in instruments section; add Alembic and migrations and Application paragraphs --- docs/article.md | 122 +++++++++++++++++++++++++++++++----------------- 1 file changed, 79 insertions(+), 43 deletions(-) diff --git a/docs/article.md b/docs/article.md index 76ff30b..fc90f68 100644 --- a/docs/article.md +++ b/docs/article.md @@ -81,9 +81,11 @@ В качестве ORM системы изначально мы рассматривали те, что встроенны в фреймворки Drogon и Oat++, но в последствии отказались от этой идеи, поскольку они ориентированы на работу в формате SQL first, а не следуя паттерну Data Mapper, как нам хотелось. Кроме того, Drogon подразумевает, что база данных создаётся и наполняется таблицами вручную через SQL, и только после этого утилита drogon_ctl подключается к базе и генерирует C++ классы моделей по уже существующей схеме. -В итоге мы остановились на ODB, поскольку, на наш взгляд, это более продвинутый инструмент, который позволяет работать с базой данных через объекты и их методы, а не писать SQL напрямую. Стоит отдельно упомянуть про лицензию, ODB распространяется под GPLv3, что подразумевает открытый исходный код. Code Synthesis, разработчик ODB, также предлагает бесплатную проприетарную лицензию (FPL) для небольших проектов, её можно получить, если объём сгенерированного кода поддержки базы данных в рамках одного релиза приложения не превышает 10 000 строк (~10-20 классов моделей). Для более крупных закрытых проектов потребуется приобрести коммерческую проприетарную лицензию (CPL). +Также, в определенный момент рассматривали использование TinyORM, но быстро отказались из-за этой идеи, поскольку, как нам показалось, проект перестал поддерживаться, ведь последний коммит в репозитории был почти 2 года назад. - +В итоге мы остановились на ODB, поскольку, на наш взгляд, это более продвинутый инструмент, который позволяет работать с базой данных с помощью объектов и их методов, а не через SQL напрямую. + +Стоит отдельно упомянуть про лицензию. ODB распространяется под GPLv2, что подразумевает открытый исходный код. В случаях, когда проект имеет лицензию более свободную, чем GPL (например, Apache или MIT), присутствует возможность получения по запросу персональной более свободной версии лицензии. Code Synthesis, разработчик ODB, также предлагает бесплатную проприетарную лицензию (FPL) для небольших проектов, её можно получить, если объём сгенерированного кода поддержки базы данных в рамках одного релиза приложения не превышает 10 000 строк (~10-20 классов моделей). Для более крупных закрытых проектов потребуется приобрести коммерческую проприетарную лицензию (CPL). ### Миграции @@ -98,40 +100,34 @@ Alembic показался нам хорошим выбором, посколь ### Структура проекта ```toml -- .devcontainer/ # Конфигурация VSCode DevContainer -- .github/ # Конфигурация пайплайна GitHub Actions -- .vscode/ # Настройки редактора и сниппеты кода для VSCode -- alembic/ # Конфигурация Alembic и история миграций -- ci/ # Конфигурация для запуска API в кластере k8s -- deps/ # Собственные Conan-рецепты зависимостей -- docs/ # Документация проекта -- profiles/ # Conan-профили под отдельные конфигурации сборки -- scripts/ # Вспомогательные скрипты -- src/ # Код приложения -- test/ # Unit и e2e тесты -- test_package/ # Служебный conanfile для проверки собранного Conan-пакета -- .clang-format # Конфигурация автоформатирования кода -- .clang-tidy # Конфигурация статического анализа кода -- .env.example # Пример файла переменных окружения -- .gitattributes # Настройки Git -- .gitignore # Список файлов и директорий, игнорируемых Git -- CMakeLists.txt # Корневой конфигурационный файл сборки -- CMakeUserPresets.json # Пресеты CMake, генерируемые Conan -- conanfile.py # Рецепт Conan-пакета API -- docker-compose.yml # Конфигурация для запуска базы данных и других сервисов -- Dockerfile # Многоступенчатая сборка продуктового образа -- LICENSE # Лицензия -- Makefile # Таргеты Makefile для быстрых команд -- README.md # Инструкции по разработке, сборке и запуску проекта +.devcontainer/ # Конфигурация VSCode DevContainer +.github/ # Конфигурация пайплайна GitHub Actions +.vscode/ # Настройки редактора и сниппеты кода для VSCode +alembic/ # Конфигурация Alembic и список миграций +ci/ # Конфигурация для запуска API в кластере k8s +deps/ # Собственные Conan-рецепты зависимостей +docs/ # Документация проекта +profiles/ # Conan-профили под отдельные конфигурации сборки +scripts/ # Вспомогательные скрипты +src/ # Код приложения +test/ # Unit и e2e тесты +test_package/ # Служебный conanfile для проверки собранного Conan-пакета +.clang-format # Конфигурация автоформатирования кода +.clang-tidy # Конфигурация статического анализа кода +.env.example # Пример файла переменных окружения +.gitattributes # Настройки Git +.gitignore # Список файлов и директорий, игнорируемых Git +CMakeLists.txt # Корневой конфигурационный файл сборки +CMakeUserPresets.json # Пресеты CMake, генерируемые Conan +conanfile.py # Рецепт Conan-пакета приложения +docker-compose.yml # Конфигурация для запуска базы данных и других сервисов +Dockerfile # Многоступенчатая сборка продуктового образа +LICENSE # Лицензия проекта +Makefile # Таргеты Makefile для быстрых команд +README.md # Основная информация о проекте, инструкции ``` - - - - - - -### Devcontainer +### DevContainer Директория `.devcontainer` содержит конфигурацию для разработки внутри Dev Container в VS Code. Сборка самого контейнера идёт на основе `Dockerfile`, который лежит в той же директории и устанавливает весь набор инструментов, таких как Clang, CMake, Conan и остальные. Там же, в `Dockerfile`, лежащий рядом Conan-профиль `to-dos-conan-profile.conf` копируется в `/root/.conan2/profiles/default`, тем самым профиль устанавливается, как профиль по умолчанию, в результате Conan подхватывает его автоматически при любой сборке, без необходимости указывать профиль вручную через флаг. @@ -139,40 +135,80 @@ Alembic показался нам хорошим выбором, посколь В том же `devcontainer.json` через хук `postCreateCommand` сразу после создания контейнера автоматически подключается локальное хранилище рецептов Conan, чтобы не приходилось выполнять эту команду вручную. -Также в конфигурации подключена [devcontainer feature](https://containers.dev/features) `docker-outside-of-docker`, которая позволяет работать с Docker на хост-машине изнутри devcontainer, а не поднимать вложенный Docker внутри контейнера. Кроме этого, использование позволяет наблюдать запущенные контейнеры через Docker Desktop, что достаточно удобно. +Дополнительно в конфигурации используется [DevContainer Feature](https://containers.dev/features) - `docker-outside-of-docker`, которая позволяет работать с Docker на хост-машине изнутри devcontainer, а не поднимать вложенный Docker внутри контейнера. Кроме этого, использование позволяет наблюдать запущенные контейнеры через Docker Desktop, что достаточно удобно. Чтобы контейнеры, поднятые на хосте, были доступны и по сети из самого devcontainer, в `runArgs` явно указан флаг `--network=host`, который подключает devcontainer к сетевому пространству хост-машины напрямую, а не изолирует его в собственной Docker-сети. ### Пакетные рецепты -Как упоминали ранее, мы используем Conan в качестве пакетного менеджера. По умолчанию, устанавливая зависимость, Conan ищет её рецепт в собственном публичном индексе, который называется `conan-center-index`. Бывает так, что это хранилище не содержит нужного пакета, поэтому Conan предусмотрел возможность подключения собственных источников рецептов, в дополнение к официальному индексу. +По умолчанию, устанавливая зависимость, Conan ищет её рецепт в собственном публичном индексе, который называется `conan-center-index`. Бывает так, что это хранилище не содержит нужного пакета, поэтому Conan предусмотрел возможность подключения собственных источников рецептов, в дополнение к официальному индексу. -Чтобы Conan смог увидеть локальные рецепты, директория, в которой находятся рецепты (в нашем случае `deps`), подключается как дополнительный локальный remote командой `conan remote add local-recipes ./deps --type=local-recipes-index`. После этого при установке зависимостей Conan берёт рецепты уже не из публичного индекса, а из локальной папки, собирает исходники под нужную конфигурацию и кладёт готовые бинарники в кэш, то есть точно так же, как если бы эти пакеты были частью conan-center-index. +Чтобы Conan смог увидеть локальные рецепты, директория, в которой находятся рецепты (в нашем случае `deps`), подключается как дополнительный локальный remote командой `conan remote add local-recipes ./deps --type=local-recipes-index`. После этого при установке зависимостей Conan берёт рецепты уже не из публичного индекса, а из локальной папки, собирает исходники под нужную конфигурацию и кладёт готовые бинарники в кэш, то есть точно так же, как если бы эти пакеты были частью `conan-center-index`. ### Тесты В проекте реализовано два вида тестов: unit и E2E. -Unit-тесты используют библиотеку GTest и вынесены в отдельную директорию `test` со своим `CMakeLists.txt`, в котором описана вся логика сборки тестового бинарника, он собирается как отдельный исполняемый файл, независимый от основного. Сборка тестов подключается в корневом `CMakeLists.txt` и может быть отключена переменной окружения `EXCLUDE_UNIT_TESTS_FROM_BUILD`. Также, сборка тестов не происходит при кросс-платформенной сборке, поскольку в этом случае не получится запустить сам исполняемый файл тестов на сборочной машине. +Unit-тесты используют библиотеку [GTest](https://google.github.io/googletest/) и вынесены в отдельную директорию `test` со своим `CMakeLists.txt`, в котором описана вся логика сборки тестового бинарника, он собирается как отдельный исполняемый файл, независимый от основного. Сборка тестов подключается в корневом `CMakeLists.txt` и может быть отключена переменной окружения `EXCLUDE_UNIT_TESTS_FROM_BUILD`. Также, сборка тестов не происходит при кросс-платформенной сборке, поскольку в этом случае не получится запустить сам исполняемый файл тестов на сборочной машине. -> На данный момент в директории лежит только один unit-тест, который служит примером. +> На данный момент в директории лежит только один unit-тест, который служит примером возможного теста. -E2E-тесты реализованны с использованием библиотеки Karate. В случае E2E, вместо изолированного бинарника с тестами Karate выполняет HTTP-запросы к уже поднятому и работающему экземпляру API. Сценарий (тест) лежит в директории `e2e`, в файле `to-dos-happy-path.feature`, и последовательно проходит через все ключевые операции, начиная от создания задачи, и заканчивая удалением и финальной проверкой, что задачи больше нет в базе. +E2E-тесты реализованны с использованием библиотеки [Karate](https://docs.karatelabs.io/). В случае E2E, вместо изолированного бинарника с тестами Karate выполняет HTTP-запросы к уже поднятому и работающему экземпляру API. Сценарий (тест) лежит в директории `e2e`, в файле `to-dos-happy-path.feature`, и последовательно проходит через все эндпоинты приложения, начиная от создания задачи, и заканчивая удалением и финальной проверкой, что задачи больше нет в базе. -Поскольку Karate запускается не как часть сборки C++ проекта, а как самостоятельный процесс, для него в той же директории `e2e` заведён отдельный `KarateDockerfile`, который использует за основу лёгкий образ с Java и скачанным `karate.jar`, не связанный с основным `Dockerfile` приложения. Такое разделение позволяет запускать e2e-тесты в двух разных окружениях: локально в `docker-compose` и в кластере k8s, где собранный образ приложения разворачивается через Helm. +Поскольку Karate запускается не как часть сборки приложения, а как самостоятельный процесс, для него в той же директории `e2e` заведён отдельный `KarateDockerfile`, который использует за основу лёгкий образ с Java и скачанным `karate.jar`, не связанный с основным `Dockerfile` приложения. Такое разделение позволяет запускать e2e-тесты в двух разных окружениях: локально в `docker-compose` и в кластере k8s, где собранный образ приложения разворачивается через Helm. ### CI-пайплайн Пайплайн состоит из двух воркфлоу, расположенных в директории `.github/workflows`. -Первый, `.reusable-docker-build-and-push.yml`, отвечает за сборку и публикацию Docker-образа API. Он выполняется в двух случаях: при пуше в `master`, тогда собирается и публикуется актуальный образ с тегом `latest`, либо по вызову из другого воркфлоу. Сама сборка идёт для двух архитектур, arm64 и amd64, затем оба образа объединяются в единый мультиархитектурный образ с помощью docker buildx. +Первый воркфлоу, `.reusable-docker-build-and-push.yml`, отвечает за сборку и публикацию Docker-образа API. Он выполняется в двух случаях: при пуше в master-ветку, тогда собирается и публикуется актуальный образ с тегом `latest`, либо по вызову из другого воркфлоу. Сама сборка идёт для двух архитектур, arm64 и amd64, затем оба образа объединяются в единый мультиархитектурный образ с помощью docker buildx. Второй воркфлоу, `e2e-tests-on-pull-request.yml`, запускается на каждый pull request и выполняет E2E-тесты. Выполнение тестов происходит в двух джобах, каждая из которых отвечает за собственное окружение. Первая джоба вызывает воркфлоу сборки образа API, дожидается публикации образа под конкретным коммитом и разворачивает его через Helm в тестовый кластер k8s, поднятый прямо в CI. Вторая джоба поднимает `docker-compose`, который собирает актуальный образ API и запускает его вместе с базой данных и контейнером Karate в общей docker-сети. ### Alembic и миграции +Вся логика, связанная с миграциями, вынесена в отдельную директорию `alembic` в корне проекта, это было сделано осознанно, чтобы не смешивать эту часть с кодом самого приложения, то есть в стороне от `src`. + +Внутри лежит основной конфигурационный файл Alembic - `alembic.ini`, который в целом остался близким к стандартному, сгенерированному самим Alembic при инициализации. Внутри этого файла можно найти параметр `script_location`, который содержит путь к директории `migrations`, где Alembic будет искать сценарий окружения и сами файлы миграций. + +Внутри директории `migrations` находится `env.py`, который предназначен для настройки окружения при каждом запуске, также здесь происходит чтение переменных среды (`POSTGRES_HOST`, `POSTGRES_PORT` и так далее), содержащих данные необходимые для подключения к базе данных, само подключение, и, если базы ещё не существует, логика автоматического создания базы перед применением миграций. Там же лежит папка `versions` с самими миграциями (на данный момент там всего одна, начальная, создающая таблицу `todo`). + +Отдельно стоит директория `models` с файлом `to_do.py`, который представляет собой Python-класс, основанный на SQLAlchemy и повторяющий структуру ODB-модели `ToDo` из C++ представления модели. Именно его `env.py` использует как `target_metadata`, когда при вызове `alembic revision --autogenerate` происходит сравнение метаданных с текущим состоянием базы данных, на основе разницы которых Alembic генерирует нужную миграцию. + ### Код приложения +Весь код приложения находится в директории `src`. + +Если говорить об архитектуре, то мы выбрали для себя 3-х уровневую архитектуру, которая состоит из: +- `src/api` - Уровень представления +- `src/application` - Уровень бизнес-логики +- `src/core` - Уровень данных + +Такой подход: +- Помогает эффективно работать с обширным набором кода, поскольку разработчик понимает, где он должен искать ту или иную логику +- Разделяет приложение на отдельные части, каждая из которых выполняет свои функции, что позволяет вести разработку каждой из частей отдельно от других +- Ограничивает распространение изменений в коде, препятствуя возникновению ошибок, упрощая тестирование и поддержку кода + +Каждый из слоев собирается как отдельная статическая библиотека и линкуется в единый исполняемый файл в корневом `CMakeLists.txt`. Такая изоляция позволяет предотвратить построение взаимосвязей между слоями на уровне компиляции, поскольку обратиться к функционалу из другого слоя становится затруднительно. + +#### Уровень представления + +Представляет собой логический уровень взаимодействия API с пользователем. Этот слой не содержит бизнес-логики и не взаимодействует с уровнем данных напрямую, а только лишь через уровень бизнес-логики. Включает в себя контроллеры, выполняя функцию обработки HTTP-запросов, также производит валидацию входных данных. + +#### Уровень бизнес-логики + +Представляет собой логический уровень выполнения бизнес-логики приложения. Код внутри сгруппирован по функционалу, где каждая директория в `application/features` описывает один конкретный сценарий использования (создание задачи, удаление и т.п.). + +Каждый такой сценарий имеет классы с чётко разделённой ответственностью. Command или Query отвечает непосредственно за обращение к базе данных, а Handler принимает Request, вызывает нужный Command или Query и формирует из результата Response. + +Также в `application` находится `db_connection` - обёртка для получения общего подключения к базе данных, которое передаётся во все Command и Query классы, `shared-dtos` - DTO, используемые сразу в нескольких фичах, и `odb-gen` - сгенерированный ODB-компилятором код, обеспечивающий саму работу с базой на основе моделей, описанных в уровне данных. + +#### Уровень данных + +Представляет собой логический уровень, описывающий структуру данных приложения. Здесь находится класс, описывающий сущность `ToDo` и размеченный ODB-прагмами, которые определяют, как объект существует таблице в базы данных (поля, их типы и т.п.). На основе этих классов моделей ODB-компилятор генерирует код для уровня бизнес-логики. + +### Docker Compose + ### Makefile ### Профили и скрипты @@ -181,7 +217,7 @@ E2E-тесты реализованны с использованием библ Одной из улучшений было заимстованно с conan. Начнем с профилей. У Conan есть понятие [профилей](https://docs.conan.io/2/reference/config_files/profiles.html). Это по сути краткий свод правил в которые входит: какой компилятор использовать, флаги компилятора, версия C++ и многое другое. В Conan в документации почти каждый раз перед сборкой проекта рекомендует запустить `conan profile detect`, что определит профиль по вашей системе. Но так как мы хотели использовать clang в качестве компилятора с 20 версией C++, то появилась мысль написать свой профиль, так как по умолчанию Conan всегда находил gcc компилятор с какой-нибудь старой версией C++. -``` +```ini [settings] # Определяет систему автоматически через detect_api (например Linux, Windows, Macos) os={{detect_api.detect_os()}} From fd74f316c1a7b3870fbf02943df46886ee6c3b59 Mon Sep 17 00:00:00 2001 From: Artem Sheptunov <106321977+Infindery@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:51:05 +0500 Subject: [PATCH 12/19] docs: #71: change project structure markdown type --- docs/article.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/article.md b/docs/article.md index fc90f68..69ea6d8 100644 --- a/docs/article.md +++ b/docs/article.md @@ -99,7 +99,7 @@ Alembic показался нам хорошим выбором, посколь ### Структура проекта -```toml +```ini .devcontainer/ # Конфигурация VSCode DevContainer .github/ # Конфигурация пайплайна GitHub Actions .vscode/ # Настройки редактора и сниппеты кода для VSCode From 9e51a3ccfef90c827e43ad6107d6546fb49ed32d Mon Sep 17 00:00:00 2001 From: Artem Sheptunov <106321977+Infindery@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:36:14 +0500 Subject: [PATCH 13/19] docs: #71: apply corrections --- docs/article.md | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/article.md b/docs/article.md index 69ea6d8..dbac303 100644 --- a/docs/article.md +++ b/docs/article.md @@ -55,15 +55,15 @@ Прежде всего мы решили посмотреть, что уже есть на эту тему - статьи, доклады, возможно, готовые проекты, которые могли бы упростить выбор и заранее подсветить подводные камни. Но довольно быстро стало понятно, что материалов на поверхности крайне мало. Статей и докладов о разработке бекенда на C++ оказалось меньше, чем мы рассчитывали, а open-source шаблоны подобного рода и вовсе редкость. -Это, впрочем, не выглядело поводом для расстройства, а скорее стало дополнительным аргументом в пользу того, что тема слабо освещена и наш шаблон, а также опыт его разработки могли оказаться действительно полезны сообществу. Тем более что мы изначально планировали делать проект открытым и делиться этим опытом с другими. И опыт этот, прежде всего, об одной идее: нам, как разработчикам, хочется, чтобы работа была устроена легко и просто - это именно то, к чему мы стремились с самого начала. +Это, впрочем, не выглядело поводом для расстройства, а скорее стало дополнительным аргументом в пользу того, что тема слабо освещена, и наш шаблон, а также опыт его разработки могли оказаться действительно полезны, тем более что мы изначально планировали делать проект открытым и делиться этим опытом с другими. И опыт этот, прежде всего, об одной идее: нам, как разработчикам, хочется, чтобы работа была устроена легко и просто - это именно то, к чему мы стремились с самого начала. ### Система сборки -Но озвученная идея разбивается о реальность, как только речь заходит о сборке проекта, а в C++ это важная часть любого проекта. В отличие от JavaScript, Python и многих других языков, в комьюнити C/C++ так и не сложилось единого подхода, но из большинства CMake стал ближе всего к негласному стандарту, поскольку он кроссплатформенный, поддерживается практически всеми IDE и компиляторами, а большинство современных библиотек уже поставляются с готовой CMake-конфигурацией, и выбор здесь напрашивался сам собой. +Но озвученная идея разбивается о реальность, как только речь заходит о сборке проекта, а в C++ это важная часть любого проекта. В отличие от JavaScript, Python и многих других языков, в комьюнити C/C++ так и не сложилось единого подхода, но из большинства CMake стал ближе всего к негласному стандарту, поскольку он кроссплатформенный, поддерживается практически всеми IDE и компиляторами, а большинство современных библиотек уже поставляются с готовой CMake-конфигурацией. Выбор здесь напрашивался сам собой. ### Пакетный менеджер -С пакетным менеджером история повторилась и всё не так однозначно, и в первую очередь мы выбирали между vcpkg и Conan. Остановились на Conan, поскольку он, в отличие от vcpkg, не ограничен связкой CMake и MSBuild, а одинаково хорошо работает с разными системами сборки и позволяет точно описывать окружение через профили - компилятор, архитектуру, ОС, тип сборки. Эта гибкость является для нас важным аспектом, ведь шаблон должен был одинаково собираться под разные архитектуры и операционные системы, а не быть заточен под одну конкретную конфигурацию. +С пакетным менеджером история повторилась. В первую очередь мы выбирали между vcpkg и Conan. Остановились на Conan, поскольку он, в отличие от vcpkg, не ограничен связкой CMake и MSBuild, а одинаково хорошо работает с разными системами сборки и позволяет точно описывать окружение (компилятор, архитектуру, ОС, тип сборки и т.п.) через профили. Эта гибкость является для нас важным аспектом, ведь шаблон должен был одинаково собираться под разные архитектуры и операционные системы, а не быть заточен под одну конкретную конфигурацию. ### Компилятор @@ -73,25 +73,25 @@ ### Веб-фреймворк -В выборе веб-фремворка мы остановились на Drogon, поскольку, как нам показалось, у него низкий порог входа и понятный синтаксис. Из минусов можем отметить документацию, поскольку некоторые разделы содержат битые ссылки, из-за чего порой было сложно проследить логику и найти нужный материал. +В выборе веб-фремворка мы остановились на Drogon, поскольку, как нам показалось, у него низкий порог входа и понятный синтаксис. Из минусов можем отметить документацию, поскольку некоторые разделы содержат битые ссылки, что порой усложняет повествование и попытки найти нужный материал. -Помимо Drogon, рассматривали Oat++ и userver. В случае Oat++, он показался нам более сложным и многословным по сравнению с Drogon. С userver ситуация другая, фреймворк отсутствует в `conan-center-index`, а его установка подразумевает самостоятельную сборку из репозитория разработчиков по их собственному шаблону, что не подходит нам. +Помимо Drogon, рассматривали Oat++ и userver. Oat++ показался нам более сложным и многословным по сравнению с Drogon. С userver ситуация другая, фреймворк отсутствует в `conan-center-index`, а его установка подразумевает самостоятельную сборку из репозитория разработчиков по их собственному шаблону, что не подходит нам. ### ORM -В качестве ORM системы изначально мы рассматривали те, что встроенны в фреймворки Drogon и Oat++, но в последствии отказались от этой идеи, поскольку они ориентированы на работу в формате SQL first, а не следуя паттерну Data Mapper, как нам хотелось. Кроме того, Drogon подразумевает, что база данных создаётся и наполняется таблицами вручную через SQL, и только после этого утилита drogon_ctl подключается к базе и генерирует C++ классы моделей по уже существующей схеме. +В качестве ORM системы изначально мы рассматривали те, что встроенны в фреймворки Drogon и Oat++, но в последствии отказались от этой идеи, поскольку они ориентированы на работу в формате SQL first, а не следуя паттерну Data Mapper, как нам хотелось. Кроме того, Drogon подразумевает, что база данных создаётся и наполняется таблицами вручную через SQL, и только после этого утилита drogon_ctl генерирует C++ классы моделей по уже существующей схеме, подключившись к базе. Также, в определенный момент рассматривали использование TinyORM, но быстро отказались из-за этой идеи, поскольку, как нам показалось, проект перестал поддерживаться, ведь последний коммит в репозитории был почти 2 года назад. В итоге мы остановились на ODB, поскольку, на наш взгляд, это более продвинутый инструмент, который позволяет работать с базой данных с помощью объектов и их методов, а не через SQL напрямую. -Стоит отдельно упомянуть про лицензию. ODB распространяется под GPLv2, что подразумевает открытый исходный код. В случаях, когда проект имеет лицензию более свободную, чем GPL (например, Apache или MIT), присутствует возможность получения по запросу персональной более свободной версии лицензии. Code Synthesis, разработчик ODB, также предлагает бесплатную проприетарную лицензию (FPL) для небольших проектов, её можно получить, если объём сгенерированного кода поддержки базы данных в рамках одного релиза приложения не превышает 10 000 строк (~10-20 классов моделей). Для более крупных закрытых проектов потребуется приобрести коммерческую проприетарную лицензию (CPL). +Стоит отдельно упомянуть про лицензию. ODB распространяется под GPLv2, что подразумевает открытый исходный код. В случаях, когда проект имеет лицензию более свободную, чем GPL (например, Apache или MIT), есть возможность по запросу получить персональную и более свободную версию лицензии. Code Synthesis, разработчик ODB, также предлагает бесплатную проприетарную лицензию (FPL) для небольших проектов, её можно получить, если объём сгенерированного кода поддержки базы данных в рамках одного релиза приложения не превышает 10 000 строк (~10-20 классов моделей). Для более крупных закрытых проектов потребуется приобрести коммерческую проприетарную лицензию (CPL). ### Миграции В момент реализации механизма применения миграций к базе данных мы еще не были знакомы с возможностями ODB в этой области. Как оказалось, с версии 2.3.0 ODB добавил поддержку ревизий схемы базы данных, включая генерацию миграций на основе изменений в моделях. Тогда же мы решили использовать отдельный инструмент - Alembic. Кроме того, мы увидели возможность показать в шаблоне пример того, как C++ API может взаимодействовать с инструментами из другого окружения, в нашем случае Python. -Alembic показался нам хорошим выбором, поскольку реализовать логику применения миграций и описать модели с его помощью оказалось довольно просто. Но стоит отметить, что для этого пришлось завести отдельное представление модели на Python, которое повторяло структуру моделей ODB. +Alembic показался нам хорошим выбором, поскольку реализовать логику применения миграций и описать модели с его помощью оказалось довольно просто, но пришлось завести отдельное представление модели на Python, которое повторяло структуру моделей ODB. ## Внутреннее устройство шаблона @@ -127,21 +127,21 @@ Makefile # Таргеты Makefile для быстрых кома README.md # Основная информация о проекте, инструкции ``` -### DevContainer +### Dev Container -Директория `.devcontainer` содержит конфигурацию для разработки внутри Dev Container в VS Code. Сборка самого контейнера идёт на основе `Dockerfile`, который лежит в той же директории и устанавливает весь набор инструментов, таких как Clang, CMake, Conan и остальные. Там же, в `Dockerfile`, лежащий рядом Conan-профиль `to-dos-conan-profile.conf` копируется в `/root/.conan2/profiles/default`, тем самым профиль устанавливается, как профиль по умолчанию, в результате Conan подхватывает его автоматически при любой сборке, без необходимости указывать профиль вручную через флаг. +Директория `.devcontainer` содержит конфигурацию для разработки внутри Dev Container в VS Code. Сборка самого контейнера идёт на основе `Dockerfile`, который лежит в той же директории и устанавливает весь набор инструментов, таких как Clang, CMake, Conan и остальные. Там же, в `Dockerfile` производится копирование в `/root/.conan2/profiles/default` лежащего рядом Conan-профиля `to-dos-conan-profile.conf`, тем самым содержимое профиля устанавливается в значение по умолчанию, в результате Conan подхватывает его автоматически при любой сборке, без необходимости указывать профиль вручную через флаг. -Чтобы не собирать зависимости и проект с нуля при каждом пересоздании контейнера, в `devcontainer.json` настроено кэширование через именованные Docker volumes. Отдельно кэшируется директория `.conan2` со скачанными и собранными Conan-пакетами и директория сборки `build`, где хранятся сгенерированные Conan файлы toolchain'а для CMake. Без этого кэширования пересборка контейнера с нуля означала и пересборку всех зависимостей заново, а это ощутимо по времени (в нашем случае сборка занимала 20+ минут). +Чтобы не собирать зависимости и проект с нуля при каждом пересоздании контейнера, в `devcontainer.json` настроено кэширование через именованные Docker volumes. Отдельно кэшируется директория `.conan2` со скачанными и собранными Conan-пакетами и директория сборки `build`, где хранятся сгенерированные Conan файлы toolchain'а для CMake. Без этого кэширования пересборка контейнера с нуля означала и пересборку всех зависимостей заново, а это существенно увеличивает время сборки (в нашем случае более 20 минут). В том же `devcontainer.json` через хук `postCreateCommand` сразу после создания контейнера автоматически подключается локальное хранилище рецептов Conan, чтобы не приходилось выполнять эту команду вручную. -Дополнительно в конфигурации используется [DevContainer Feature](https://containers.dev/features) - `docker-outside-of-docker`, которая позволяет работать с Docker на хост-машине изнутри devcontainer, а не поднимать вложенный Docker внутри контейнера. Кроме этого, использование позволяет наблюдать запущенные контейнеры через Docker Desktop, что достаточно удобно. +Дополнительно мы использовали [Dev Container Feature](https://containers.dev/features) - `docker-outside-of-docker`, которая позволяет работать с Docker на хост-машине изнутри devcontainer, а не поднимать вложенный Docker внутри контейнера. Кроме того, её использование позволяет наблюдать запущенные контейнеры через Docker Desktop, что достаточно удобно. -Чтобы контейнеры, поднятые на хосте, были доступны и по сети из самого devcontainer, в `runArgs` явно указан флаг `--network=host`, который подключает devcontainer к сетевому пространству хост-машины напрямую, а не изолирует его в собственной Docker-сети. +Чтобы контейнеры, поднятые на хосте, были доступны и по сети из самого devcontainer, в `runArgs` явно указан флаг `--network=host`, который подключает devcontainer к сети хост-машины напрямую, а не изолирует его в собственной Docker-сети. ### Пакетные рецепты -По умолчанию, устанавливая зависимость, Conan ищет её рецепт в собственном публичном индексе, который называется `conan-center-index`. Бывает так, что это хранилище не содержит нужного пакета, поэтому Conan предусмотрел возможность подключения собственных источников рецептов, в дополнение к официальному индексу. +По умолчанию, устанавливая зависимость, Conan ищет её рецепт в собственном публичном индексе, который называется `conan-center-index`. Бывает так, что это хранилище не содержит нужного пакета, поэтому в Conan предусмотрена возможность подключения собственных источников рецептов, в дополнение к официальному индексу. Чтобы Conan смог увидеть локальные рецепты, директория, в которой находятся рецепты (в нашем случае `deps`), подключается как дополнительный локальный remote командой `conan remote add local-recipes ./deps --type=local-recipes-index`. После этого при установке зависимостей Conan берёт рецепты уже не из публичного индекса, а из локальной папки, собирает исходники под нужную конфигурацию и кладёт готовые бинарники в кэш, то есть точно так же, как если бы эти пакеты были частью `conan-center-index`. @@ -149,31 +149,31 @@ README.md # Основная информация о проект В проекте реализовано два вида тестов: unit и E2E. -Unit-тесты используют библиотеку [GTest](https://google.github.io/googletest/) и вынесены в отдельную директорию `test` со своим `CMakeLists.txt`, в котором описана вся логика сборки тестового бинарника, он собирается как отдельный исполняемый файл, независимый от основного. Сборка тестов подключается в корневом `CMakeLists.txt` и может быть отключена переменной окружения `EXCLUDE_UNIT_TESTS_FROM_BUILD`. Также, сборка тестов не происходит при кросс-платформенной сборке, поскольку в этом случае не получится запустить сам исполняемый файл тестов на сборочной машине. +Unit-тесты используют библиотеку [GTest](https://google.github.io/googletest/) и вынесены в отдельную директорию `test` со своим `CMakeLists.txt`, в котором описана вся логика сборки тестового бинарника, он собирается как отдельный исполняемый файл, независимый от основного. Сборка тестов подключается в корневом `CMakeLists.txt` и может быть отключена переменной окружения `EXCLUDE_UNIT_TESTS_FROM_BUILD`, установленной в значение `true`. Также, сборка тестов не происходит при кросс-платформенной сборке, поскольку в этом случае не получится запустить сам исполняемый файл тестов на машине, где происходила сборка. > На данный момент в директории лежит только один unit-тест, который служит примером возможного теста. E2E-тесты реализованны с использованием библиотеки [Karate](https://docs.karatelabs.io/). В случае E2E, вместо изолированного бинарника с тестами Karate выполняет HTTP-запросы к уже поднятому и работающему экземпляру API. Сценарий (тест) лежит в директории `e2e`, в файле `to-dos-happy-path.feature`, и последовательно проходит через все эндпоинты приложения, начиная от создания задачи, и заканчивая удалением и финальной проверкой, что задачи больше нет в базе. -Поскольку Karate запускается не как часть сборки приложения, а как самостоятельный процесс, для него в той же директории `e2e` заведён отдельный `KarateDockerfile`, который использует за основу лёгкий образ с Java и скачанным `karate.jar`, не связанный с основным `Dockerfile` приложения. Такое разделение позволяет запускать e2e-тесты в двух разных окружениях: локально в `docker-compose` и в кластере k8s, где собранный образ приложения разворачивается через Helm. +Поскольку Karate запускается не как часть сборки приложения, а как самостоятельный процесс, для него в той же директории `e2e` заведён отдельный `KarateDockerfile`, который использует за основу лёгкий образ с Java и скачанным `karate.jar`, не связанный с основным `Dockerfile` приложения. Такое разделение позволяет запускать e2e-тесты в двух разных окружениях: в docker-compose и в кластере k8s, где собранный образ приложения разворачивается через Helm. ### CI-пайплайн Пайплайн состоит из двух воркфлоу, расположенных в директории `.github/workflows`. -Первый воркфлоу, `.reusable-docker-build-and-push.yml`, отвечает за сборку и публикацию Docker-образа API. Он выполняется в двух случаях: при пуше в master-ветку, тогда собирается и публикуется актуальный образ с тегом `latest`, либо по вызову из другого воркфлоу. Сама сборка идёт для двух архитектур, arm64 и amd64, затем оба образа объединяются в единый мультиархитектурный образ с помощью docker buildx. +Первый воркфлоу, `.reusable-docker-build-and-push.yml`, отвечает за сборку и публикацию Docker-образа. Он выполняется в двух случаях: при пуше в master-ветку, тогда собирается и публикуется актуальный образ с тегом `latest`, либо по вызову из другого воркфлоу. Сама сборка происходит для двух архитектур, arm64 и amd64, затем оба образа объединяются в единый мультиархитектурный образ с помощью docker buildx. -Второй воркфлоу, `e2e-tests-on-pull-request.yml`, запускается на каждый pull request и выполняет E2E-тесты. Выполнение тестов происходит в двух джобах, каждая из которых отвечает за собственное окружение. Первая джоба вызывает воркфлоу сборки образа API, дожидается публикации образа под конкретным коммитом и разворачивает его через Helm в тестовый кластер k8s, поднятый прямо в CI. Вторая джоба поднимает `docker-compose`, который собирает актуальный образ API и запускает его вместе с базой данных и контейнером Karate в общей docker-сети. +Второй воркфлоу, `e2e-tests-on-pull-request.yml`, запускается на каждый pull request и выполняет E2E-тесты. Выполнение тестов происходит в двух джобах, каждая из которых отвечает за собственное окружение. Первая джоба вызывает воркфлоу сборки образа API, дожидается публикации образа под конкретным коммитом и разворачивает его через Helm в тестовый кластер k8s, поднятый прямо в CI. Вторая джоба поднимает `docker-compose`, который собирает актуальный образ приложения и запускает его вместе с базой данных и контейнером Karate в общей Docker-сети. ### Alembic и миграции -Вся логика, связанная с миграциями, вынесена в отдельную директорию `alembic` в корне проекта, это было сделано осознанно, чтобы не смешивать эту часть с кодом самого приложения, то есть в стороне от `src`. +Вся логика, связанная с миграциями, вынесена в отдельную директорию `alembic` в корне проекта, это было сделано осознанно, чтобы не смешивать ее с кодом самого приложения, то есть в стороне от `src`. Внутри лежит основной конфигурационный файл Alembic - `alembic.ini`, который в целом остался близким к стандартному, сгенерированному самим Alembic при инициализации. Внутри этого файла можно найти параметр `script_location`, который содержит путь к директории `migrations`, где Alembic будет искать сценарий окружения и сами файлы миграций. Внутри директории `migrations` находится `env.py`, который предназначен для настройки окружения при каждом запуске, также здесь происходит чтение переменных среды (`POSTGRES_HOST`, `POSTGRES_PORT` и так далее), содержащих данные необходимые для подключения к базе данных, само подключение, и, если базы ещё не существует, логика автоматического создания базы перед применением миграций. Там же лежит папка `versions` с самими миграциями (на данный момент там всего одна, начальная, создающая таблицу `todo`). -Отдельно стоит директория `models` с файлом `to_do.py`, который представляет собой Python-класс, основанный на SQLAlchemy и повторяющий структуру ODB-модели `ToDo` из C++ представления модели. Именно его `env.py` использует как `target_metadata`, когда при вызове `alembic revision --autogenerate` происходит сравнение метаданных с текущим состоянием базы данных, на основе разницы которых Alembic генерирует нужную миграцию. +Отдельно существует директория `models` с файлом `to_do.py`, который представляет собой Python-класс, основанный на SQLAlchemy и повторяющий структуру ODB-модели `ToDo` из C++ представления модели. Именно его `env.py` использует как `target_metadata`, когда при вызове `alembic revision --autogenerate` производит сравнение метаданных с текущим состоянием базы данных, на основе разницы которых Alembic генерирует новую миграцию. ### Код приложения @@ -193,7 +193,7 @@ E2E-тесты реализованны с использованием библ #### Уровень представления -Представляет собой логический уровень взаимодействия API с пользователем. Этот слой не содержит бизнес-логики и не взаимодействует с уровнем данных напрямую, а только лишь через уровень бизнес-логики. Включает в себя контроллеры, выполняя функцию обработки HTTP-запросов, также производит валидацию входных данных. +Представляет собой логический уровень взаимодействия API с пользователем. Этот слой не содержит бизнес-логики и не взаимодействует с уровнем данных напрямую, а только лишь через уровень бизнес-логики. Включает в себя контроллеры, выполняя функцию обработчика HTTP-запросов. #### Уровень бизнес-логики @@ -201,7 +201,7 @@ E2E-тесты реализованны с использованием библ Каждый такой сценарий имеет классы с чётко разделённой ответственностью. Command или Query отвечает непосредственно за обращение к базе данных, а Handler принимает Request, вызывает нужный Command или Query и формирует из результата Response. -Также в `application` находится `db_connection` - обёртка для получения общего подключения к базе данных, которое передаётся во все Command и Query классы, `shared-dtos` - DTO, используемые сразу в нескольких фичах, и `odb-gen` - сгенерированный ODB-компилятором код, обеспечивающий саму работу с базой на основе моделей, описанных в уровне данных. +Также в `application` находится `db_connection`, представляющий собой обертку для получения общего подключения к базе данных, которое в последствии передаётся во все Command и Query классы, `shared-dtos` являются DTO, используемые сразу в нескольких сценариях, и `odb-gen`, который содержит сгенерированный ODB-компилятором код, обеспечивающий саму работу с базой на основе моделей, описанных в уровне данных. #### Уровень данных From fd873c46f2e34412c9697ecf9849469267e1dd1f Mon Sep 17 00:00:00 2001 From: Artem Sheptunov <106321977+Infindery@users.noreply.github.com> Date: Fri, 3 Jul 2026 09:37:50 +0500 Subject: [PATCH 14/19] docs: #71: apply corrections; add docker compose paragraph to internal structure section --- docs/article.md | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/docs/article.md b/docs/article.md index dbac303..95def43 100644 --- a/docs/article.md +++ b/docs/article.md @@ -110,7 +110,8 @@ docs/ # Документация проекта profiles/ # Conan-профили под отдельные конфигурации сборки scripts/ # Вспомогательные скрипты src/ # Код приложения -test/ # Unit и e2e тесты +unit/ # Unit тесты +e2e/ # E2E тесты test_package/ # Служебный conanfile для проверки собранного Conan-пакета .clang-format # Конфигурация автоформатирования кода .clang-tidy # Конфигурация статического анализа кода @@ -149,11 +150,11 @@ README.md # Основная информация о проект В проекте реализовано два вида тестов: unit и E2E. -Unit-тесты используют библиотеку [GTest](https://google.github.io/googletest/) и вынесены в отдельную директорию `test` со своим `CMakeLists.txt`, в котором описана вся логика сборки тестового бинарника, он собирается как отдельный исполняемый файл, независимый от основного. Сборка тестов подключается в корневом `CMakeLists.txt` и может быть отключена переменной окружения `EXCLUDE_UNIT_TESTS_FROM_BUILD`, установленной в значение `true`. Также, сборка тестов не происходит при кросс-платформенной сборке, поскольку в этом случае не получится запустить сам исполняемый файл тестов на машине, где происходила сборка. +Unit-тесты используют библиотеку [GTest](https://google.github.io/googletest/) и вынесены в отдельную директорию `unit` со своим `CMakeLists.txt`, в котором описана вся логика сборки тестового бинарника, он собирается как отдельный исполняемый файл, независимый от основного. Сборка тестов подключается в корневом `CMakeLists.txt` и может быть отключена переменной окружения `EXCLUDE_UNIT_TESTS_FROM_BUILD`, установленной в значение `true`. Также, сборка тестов не происходит при кросс-платформенной сборке, поскольку в этом случае не получится запустить сам исполняемый файл тестов на машине, где происходила сборка. > На данный момент в директории лежит только один unit-тест, который служит примером возможного теста. -E2E-тесты реализованны с использованием библиотеки [Karate](https://docs.karatelabs.io/). В случае E2E, вместо изолированного бинарника с тестами Karate выполняет HTTP-запросы к уже поднятому и работающему экземпляру API. Сценарий (тест) лежит в директории `e2e`, в файле `to-dos-happy-path.feature`, и последовательно проходит через все эндпоинты приложения, начиная от создания задачи, и заканчивая удалением и финальной проверкой, что задачи больше нет в базе. +E2E-тесты реализованны с использованием библиотеки [Karate](https://docs.karatelabs.io/) и вынесены в отдельную директорию `e2e`. В случае E2E, вместо изолированного бинарника с тестами Karate выполняет HTTP-запросы к уже поднятому и работающему экземпляру API. Сценарий (тест) лежит в директории `e2e`, в файле `to-dos-happy-path.feature`, и последовательно проходит через все эндпоинты приложения, начиная от создания задачи, и заканчивая удалением и финальной проверкой, что задачи больше нет в базе. Поскольку Karate запускается не как часть сборки приложения, а как самостоятельный процесс, для него в той же директории `e2e` заведён отдельный `KarateDockerfile`, который использует за основу лёгкий образ с Java и скачанным `karate.jar`, не связанный с основным `Dockerfile` приложения. Такое разделение позволяет запускать e2e-тесты в двух разных окружениях: в docker-compose и в кластере k8s, где собранный образ приложения разворачивается через Helm. @@ -205,10 +206,31 @@ E2E-тесты реализованны с использованием библ #### Уровень данных -Представляет собой логический уровень, описывающий структуру данных приложения. Здесь находится класс, описывающий сущность `ToDo` и размеченный ODB-прагмами, которые определяют, как объект существует таблице в базы данных (поля, их типы и т.п.). На основе этих классов моделей ODB-компилятор генерирует код для уровня бизнес-логики. +Представляет собой логический уровень, описывающий структуру данных приложения. Здесь находится класс, описывающий сущность `ToDo`, размеченную ODB-прагмами, которые определяют, как объект существует таблице в базы данных (поля, их типы и т.п.). На основе этих классов моделей ODB-компилятор генерирует код для уровня бизнес-логики. ### Docker Compose +В проекте есть `docker-compose.yml`, который описывает конфигурацию для запуска 4-х сервисов: +- `to-dos-api-cpp-db` +- `to-dos-api-cpp-pgadmin` +- `to-dos-api-cpp` +- `to-dos-api-cpp-karate-tests` + +Все они объединены в одну общую сеть, что позволяет сервисам обращаться друг к другу по имени. + +Сервис `to-dos-api-cpp-db` - это локальный PostgreSQL, который используется приложением в качестве базы данных. Для контейнера определен healthcheck, на статус которого ориентируются остальные сервисы, зависящие от базы. + +Сервис `to-dos-api-cpp-pgadmin` добавляет встроенное решение для просмотра базы данных. При старте pgAdmin автоматически подключается к базе данных, используя смонтированный файл `servers.json`, в котором заранее прописаны параметры подключения. + +Сервис `to-dos-api-cpp` использует в качестве основы тот же `Dockerfile`, что применяется и для сборки продуктового образа приложения, но с флагом `EXCLUDE_UNIT_TESTS_FROM_BUILD=true`, который исключает unit-тесты из сборки. У сервиса также переопределена точка входа, перед запуском самого приложения сначала применяются миграции через Alembic, и лишь после этого запускается API. Этот контейнер выступает целью для обращений сервиса `to-dos-api-cpp-karate-tests`. + +Сервис `to-dos-api-cpp-karate-tests` собирается на основе образа с JRE, необходимого для запуска библиотеки тестирования Karate, которая распространяется в виде обычного `.jar`-файла. Сам `karate.jar` скачивается при сборке образа. После старта контейнера запускается выполнение e2e-сценария `to-dos-happy-path.feature`, который делает запросы к контейнеру `to-dos-api-cpp` и завершает работу с кодом, соответствующим результату выполнения тестов. + +Конфигурация также содержит два профиля, позволяющих поднимать разный набор сервисов в зависимости от задачи: +- `DbOnly` - используется при локальной разработке и запускает только `to-dos-api-cpp-db` и `to-dos-api-cpp-pgadmin`; +- `MockForPullRequest` - используется для запуска e2e-тестов в среде docker compose в CI-пайплайне, в воркфлоу `e2e-tests-on-pull-request.yml`. + + ### Makefile ### Профили и скрипты From 51b1efd464eb43aff56e3dc52e51ef3922f91df8 Mon Sep 17 00:00:00 2001 From: Oleg Kl Date: Fri, 3 Jul 2026 15:30:38 +0500 Subject: [PATCH 15/19] docs: #71: add makefile description --- docs/article.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/article.md b/docs/article.md index 95def43..a9a22c9 100644 --- a/docs/article.md +++ b/docs/article.md @@ -233,6 +233,37 @@ E2E-тесты реализованны с использованием библ ### Makefile +Для упрощения запуска миграций как в локальной разработке так и в CI был создан Makefile. + +Вообще базово Makefile это файл с правилами, по которым утилита make автоматически собирает и работает проектом. Главное его преимущество в targets, если описать проще, то каждый target это вызов отдельного блока с командами, которые выполняют необходимые действия. А так же утилита make, которая читает этот Makefile предустановлена почти во все UNIX системы. + +Если возвращаться к проекту, то итоговы Makefile у нас выглядит так: +```makefile +# This is necessary so that environment variables from .env +# are visible when Makefile commands are executed +include .env +export + +# Generate a new Alembic migration with autogenerate +create-migration: + @cd ./alembic && \ + alembic revision --autogenerate -m $(name) + +# Apply all pending Alembic migrations +apply-migrations: + @cd ./alembic && \ + alembic upgrade head + +# Build the project, apply migrations and start the application +run: apply-migrations + @conan build . --build=missig && \ + ./build/Debug/to-dos-api + +# Run clang-tidy static analysis +run-tidy: + @run-clang-tidy -p build/Debug +``` + ### Профили и скрипты Хочется обратить внимание на некоторые тонкости, которые были внутри шаблона. From 8e6b66264c143cd26072b86c7c8855088ff715f3 Mon Sep 17 00:00:00 2001 From: Oleg Kl Date: Fri, 3 Jul 2026 15:37:09 +0500 Subject: [PATCH 16/19] docs: #71: add more details about caching conan deps --- docs/article.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/article.md b/docs/article.md index a9a22c9..631f559 100644 --- a/docs/article.md +++ b/docs/article.md @@ -366,5 +366,18 @@ def generate(self): Скорость сборки при это существенно падала. При сборке локально, было приемлемо подождать один раз, пока Conan соберет исходники всех подключаемых библиотек и соберет их конкретно под машину, а потом использовать эти артефакты собрки дальше, без необходимости их пересобирать. А вот при сборке в Github Actions это происходит каждый запуск. И на сборку хотя бы двух архитектур (arm64 и x86-64) только на linux уходило около 30 минут. -Немного подумав пришли к решению, которое нас устравало в локальной разработке - хранить артефакты сборки библиотек от Conan. Для этого в Github Actions кешировались зависимости сборки из папки `conan2/` и использовались при каждом последующем запуске. +Немного подумав, мы пришли к решению, которое нас устраивало в локальной разработке — хранить артефакты сборки библиотек от Conan. Поскольку сборка идёт внутри Docker-образа, кешировать стали не саму папку `conan2/` на раннере, а Docker-слои через `cache-from/cache-to` с `mode=max`, отдельно для каждой архитектуры: + +``` +... +cache-from: type=registry,ref=${{ env.REGISTRY_IMAGE }}:cache-amd64-${{ env.BRANCH_NAME }} +cache-to: type=registry,ref=${{ env.REGISTRY_IMAGE }}:cache-amd64-${{ env.BRANCH_NAME }},mode=max +... +``` + +Для arm64-джобы используется отдельный тег `cache-arm64-${{ env.BRANCH_NAME }}`, чтобы кеши Conan-пакетов разных архитектур не смешивались. `mode=max` кеширует каждый слой Dockerfile отдельно, поэтому слой со сборкой Conan-зависимостей переиспользуется, пока не менялись conanfile и версии библиотек. + +Кеш при этом ещё и привязан к ветке через BRANCH_NAME. Благодаря этому разные ветки не перетирают чужой кеш друг у друга, а PR-сборки могут переиспользовать Conan-артефакты, собранные ранее в рамках той же ветки. + + From 7d8dd412eb8022a2c81f6f88d11b7872368a1500 Mon Sep 17 00:00:00 2001 From: Artem Sheptunov <106321977+Infindery@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:04:10 +0500 Subject: [PATCH 17/19] dosc: #71: add conclusion --- docs/article.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/article.md b/docs/article.md index 631f559..fb74a01 100644 --- a/docs/article.md +++ b/docs/article.md @@ -379,5 +379,8 @@ cache-to: type=registry,ref=${{ env.REGISTRY_IMAGE }}:cache-amd64-${{ env.BRANCH Кеш при этом ещё и привязан к ветке через BRANCH_NAME. Благодаря этому разные ветки не перетирают чужой кеш друг у друга, а PR-сборки могут переиспользовать Conan-артефакты, собранные ранее в рамках той же ветки. +## Вывод +Мы начали с идеи о том, что запуск нового C++ API-проекта не должен превращаться в рутину, состоящей из настройки сборки, поиска подходящих библиотек, конфигурирование окружения с нуля и т.п.. Пройдя путь от выбора инструментов до столкновения с реальными подводными камнями, мы получили рабочий шаблон, который решает именно эту задачу, когда после нажатия пары кнопок у вас уже есть настроенная инфраструктура, тесты, CI и миграции. +Надеемся, что опыт, который мы получили в процессе, может быть полезным не только нам, поэтому намеренно сделали проект открытым, так что если вы задумываетесь о старте C++ API-проекта, то попробуйте наш шаблон, а если найдёте, что улучшить, то будем рады issue или PR'у. \ No newline at end of file From fe90c07852285c479f6ba2855a5b6a0e95c4b2a8 Mon Sep 17 00:00:00 2001 From: Artem Sheptunov <106321977+Infindery@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:08:22 +0500 Subject: [PATCH 18/19] docs: #71: remove redundant docker compose services enumeration --- docs/article.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/article.md b/docs/article.md index fb74a01..ca97b25 100644 --- a/docs/article.md +++ b/docs/article.md @@ -210,13 +210,7 @@ E2E-тесты реализованны с использованием библ ### Docker Compose -В проекте есть `docker-compose.yml`, который описывает конфигурацию для запуска 4-х сервисов: -- `to-dos-api-cpp-db` -- `to-dos-api-cpp-pgadmin` -- `to-dos-api-cpp` -- `to-dos-api-cpp-karate-tests` - -Все они объединены в одну общую сеть, что позволяет сервисам обращаться друг к другу по имени. +В проекте есть `docker-compose.yml`, который описывает конфигурацию для запуска 4-х сервисов. Все они объединены в одну общую сеть, что позволяет сервисам обращаться друг к другу по имени. Сервис `to-dos-api-cpp-db` - это локальный PostgreSQL, который используется приложением в качестве базы данных. Для контейнера определен healthcheck, на статус которого ориентируются остальные сервисы, зависящие от базы. From ef5fc8d8ca25cdc2ef78bbfe39779c81f61651c6 Mon Sep 17 00:00:00 2001 From: Oleg Kl Date: Fri, 3 Jul 2026 16:12:23 +0500 Subject: [PATCH 19/19] dosc: #71: rephrase makefile part --- docs/article.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/article.md b/docs/article.md index ca97b25..55e6859 100644 --- a/docs/article.md +++ b/docs/article.md @@ -227,9 +227,9 @@ E2E-тесты реализованны с использованием библ ### Makefile -Для упрощения запуска миграций как в локальной разработке так и в CI был создан Makefile. +С целью улучшения опыта разработки, в шаблон был добавлен Makefile. -Вообще базово Makefile это файл с правилами, по которым утилита make автоматически собирает и работает проектом. Главное его преимущество в targets, если описать проще, то каждый target это вызов отдельного блока с командами, которые выполняют необходимые действия. А так же утилита make, которая читает этот Makefile предустановлена почти во все UNIX системы. +Makefile группирует в себе наборы команд, необходимых для выполнения различных сценариев, таких как применение миграций или запуск приложения. Такие сценарии именуются targets и могут быть вызваны как локально, так и из CI-пайплайна. Также, чаще всего утилита make, которая исполняет правила, описанные в Makefile, уже предустановлена в UNIX-подобные системы. Если возвращаться к проекту, то итоговы Makefile у нас выглядит так: ```makefile