Перейти к содержимому

Приложения и общие библиотеки в отдельных репозиториях

Эта страница переведена с английского. Читать страницу на английском

Zeyn работает в одном репозитории, но многие команды держат каждое приложение в своём репозитории, а общий код (entity, сервисы, целые feature) выносят в библиотеки, которыми пользуются несколько приложений. Это руководство показывает, как всё настроить, и одно правило, благодаря которому это работает.

Что где лежит

РепозиторийСоздаётся командойСодержит
Приложениеzeyn new billingmain.rs, app.rs, feature приложения, конфигурацию, каталоги приложения
Общая библиотекаzeyn new acme-domain --libentity, сервисы, feature, их каталоги, CATALOGS

Feature библиотеки ничем не отличается от feature приложения: модели, action, экраны, права и переводы лежат вместе в одной папке. Приложение решает, какие feature библиотеки оно подключает.

let i18n = I18n::new("en")
    .catalogs(acme_domain::CATALOGS)?
    .catalog("en", include_str!("../locales/en.json"))?
    .catalog("tr", include_str!("../locales/tr.json"))?;

App::new(t!("app.name"))
    .i18n(i18n)
    .service(acme_domain::customers::CustomerStore::default())
    .feature(acme_domain::customers::feature())

Каталоги библиотек загружаются первыми, поэтому приложение может переопределить строку библиотеки, задав тот же ключ. Начинайте ключи библиотек с имени feature (customers.list), чтобы две библиотеки никогда не сталкивались.

examples/shared-domain это такая библиотека. examples/billing и examples/crm оба подключают её feature customers; crm переименовывает «Customers» в «Accounts» в своём каталоге.

Чем должна владеть библиотека

Правило сирот (orphan rule) в Rust гласит, что трейт можно реализовать только в крейте, который определяет трейт или тип. Приложение не может реализовать zeyn::Model или zeyn::Entity для типа из библиотеки, поэтому:

  • Ставьте #[entity], #[model] и #[choice] на тип в библиотеке, рядом с его правилами.
  • Держите правила валидации (#[rule(...)]) на общем типе. Тогда каждое приложение проверяет одинаково.
  • Приложение, которому нужно дополнительное поведение, оборачивает тип (struct Invoice { customer: Customer, ... }) или добавляет собственный трейт. Оно не правит тип библиотеки.

Как приложения получают библиотеку

ВариантПлюсыНа что обратить внимание
Git-зависимость с тегомНичего не нужно запускать, работает с любым Git-серверомCargo забирает весь репозиторий; каждое приложение указывает URL
Приватный реестр Cargo (Kellnr, реестр пакетов GitLab и другие)Обычные зависимости version = "0.3", разрешение semverСервис, который нужно запускать, резервировать и защищать
Один репозиторий-workspace для всегоОдна сборка, один Cargo.lock, рефакторинг одним коммитомКаждая команда видит каждое приложение; доступ регулируется на уровне репозитория, а не папки

Начните с Git-тегов. Переходите на реестр, когда библиотек становится больше пары или когда разбор версий вручную становится утомительным. Код приложения не меняется; меняются только строки в Cargo.toml.

[dependencies]
zeyn = { git = "https://git.example.com/platform/zeyn.git", tag = "v0.0.2" }
acme-domain = { git = "https://git.example.com/shared/acme-domain.git", tag = "v0.3.0" }

zeyn new по умолчанию записывает zeyn = "<version>". Пока Zeyn не опубликован, замените эту строку на Git-форму выше в каждом новом приложении и библиотеке.

Правило: одна версия Zeyn везде

Каждая библиотека и каждое приложение должны зависеть ровно от одного и того же Zeyn. Для Git-зависимостей это значит тот же URL и тот же тег.

Cargo считает два разных источника Zeyn двумя разными крейтами. Библиотека, собранная с тегом v0.0.2, возвращает zeyn::Feature, который приложение, собранное с v0.0.3, принять не может, и компилятор сообщает о несоответствии типов между двумя сущностями с одним и тем же именем. Реестр смягчает это до «той же совместимой по semver версии», но правило остаётся: один Zeyn на сборку.

Чтобы обновить Zeyn:

  1. Обновите тег в каждой библиотеке, запустите её тесты, выпустите новый тег библиотеки.
  2. Обновите Zeyn и теги библиотек в каждом приложении вместе, одним коммитом.

cargo tree -d -i zeyn перечисляет дубликаты. Если он выводит больше одного Zeyn, какая-то зависимость всё ещё указывает на старый тег.

Шаг за шагом

  1. Создайте библиотеку и отправьте её в отдельный репозиторий.
   zeyn new acme-domain --lib
   cd acme-domain
   cargo test
   git init && git add . && git commit -m "First version"
   git tag v0.1.0 && git push origin main --tags
  1. Добавьте её в Cargo.toml приложения с тегом и подключите её каталоги, сервисы и feature, как показано выше.
  2. Разрешите CI приложения читать библиотеку. Сгенерированный .gitlab-ci.yml переписывает URL https://<gitlab host>/ и ssh://git@<gitlab host>/ так, чтобы использовать токен задания. В GitLab откройте проект библиотеки, затем настройки CI/CD, Job token permissions, и добавьте проект приложения в список разрешённых.
  3. Запускайте app.check() в тесте приложения (сгенерированный tests/app.rs это делает). Он падает, когда у подключённой feature есть структурные проблемы, например ключ перевода, которого нет в одном из языков.

Работа над библиотекой и приложением одновременно

Направьте приложение на ваш локальный checkout, не редактируя Cargo.toml. Поместите это в .cargo/config.toml приложения и не добавляйте этот файл в Git:

[patch."https://git.example.com/shared/acme-domain.git"]
acme-domain = { path = "../acme-domain" }

Когда изменение готово, уберите это, поставьте тег на библиотеку и обновите тег в приложении.

Выпуск библиотеки

  1. Повысьте version в Cargo.toml.
  2. Запустите cargo semver-checks, если он установлен. Он сообщает об изменениях публичного API, ломающих вызывающий код. Ломающее изменение требует новой минорной версии, пока библиотека ниже 1.0.
  3. Закоммитьте, поставьте тег v<version>, отправьте тег. Задание CI tag-matches-version падает, когда тег и Cargo.toml расходятся.