Создание компонентов

Компонент — это переиспользуемый модуль Wippy: слайс функциональности, опубликованный в хаб и монтируемый в хост-приложение. Проблема компонента в том, что он не может назвать то, от чего зависит: ему нужна какая-то база данных, какой-то process host, какой-то роутер, но он не знает, какие именно даст ему хост. Wippy решает это через интерфейс требований — компонент объявляет дыры, хост их заполняет.

Это руководство описывает сторону автора: объявление такого интерфейса и понимание того, как значения попадают в ваши записи. О стороне потребителя (lock-файлы, ограничения версий, wippy add/update) см. Управление зависимостями. О том, как компонент устроен внутри, см. Архитектура приложения.

Три типа записей

Тип Сторона Роль
ns.definition компонент Метаданные модуля; обязательна для публикации.
ns.requirement компонент Дыра, которую должен заполнить хост, и куда внедрить значение.
ns.dependency хост Монтирует компонент и передаёт значения для его требований.

ns.definition

Одна на модуль, обязательна для публикации. Несёт отображаемое имя модуля и путь к README — ничего больше.

- name: definition
  kind: ns.definition
  module: jobs                # optional; defaults to the entry name
  readme: file://README.md    # path to the module's documentation
  meta:
    title: Durable Jobs
    description: Leased job queue with retry and dead-lettering.

Данными компонента являются только module и readme; meta — обычные метаданные записи для управляющих UI. Заметки к релизу передаются во время публикации, а не здесь.

ns.requirement

Требование — это именованная дыра со списком целей внедрения. Хост передаёт значение; среда выполнения записывает это значение в каждую целевую запись по указанному пути.

- name: target_db
  kind: ns.requirement
  meta:
    description: SQL database backing every table in this module.
  default: app:db
  targets:
    - entry: app.jobs.migrations:schema
      path: .meta.target_db
    - entry: app.jobs.persist:lifecycle
      path: .db

default — обязательное или необязательное

Поле default решает, обязан ли хост передать значение:

  • default присутствует (любое значение, включая пустую строку) → требование необязательное. Если хост ничего не передал, используется значение по умолчанию.
  • default отсутствует → требование обязательное. Если ничего не передано, компоновка падает в строгом режиме (и предупреждает в остальных).
Явно пустое значение по умолчанию (default: "") отличается от отсутствия default вовсе. Пустая строка означает «необязательно, откатывается в ничто»; отсутствие означает «хост обязан это предоставить». Используйте default для инфраструктуры с разумной внутриприложенческой конвенцией (app:db, app:processes); опускайте его для значений, которые может знать только хост.

targets — куда попадает значение

Каждая цель — пара {entry, path}:

  • entry — запись, в которую внедряется значение. Голое имя (schema) разрешается внутри пространства имён самого требования; полностью квалифицированный id (app.jobs.migrations:schema) указывает ровно на эту запись, через пространства имён.
  • path — точечный путь внутрь целевой записи, например .meta.target_db, .host, .database.url. Ведущая точка — конвенция.

Требование без целей — ошибка: дыра, которая никуда не внедряет, бессмысленна.

Суффикс += у пути добавляет вместо установки — полезно, когда несколько требований наполняют один список (например, middleware):

targets:
  - entry: app.api:router
    path: .middleware+=     # appends the value to the list at .middleware

Одно требование, много целей

Группируйте под одним требованием всё, чему нужно одно и то же значение. Это идиоматический паттерн: требование target_db, внедряющее в .meta.target_db каждой миграции и в .db каждой библиотеки персистентности; process_host, внедряющее в .host каждого супервизируемого service; api_router, внедряющее в .meta.router каждого эндпоинта:

- name: process_host
  kind: ns.requirement
  default: app:processes
  targets:
    - { entry: app.jobs.service:worker.service, path: .host }
    - { entry: app.jobs.service:sweeper.service, path: .host }

Хост заполняет одну дыру; среда выполнения разносит значение по всем целям. Ничего не зеркалится в параллельную конфигурационную запись — запись требования и есть проводка.

Потребление компонента

Хост монтирует компонент через ns.dependency и заполняет его требования через parameters:

version: "1.0"
namespace: app
entries:
  - name: dep.jobs
    kind: ns.dependency
    component: acme/jobs
    version: "^1.0.0"
    parameters:
      - name: target_db
        value: app:db
      - name: process_host
        value: app:processes
      - name: api_router
        value: app:api

Каждый parameter.name соответствует требованию; его value — то, что внедряется в цели этого требования. Требования со значением по умолчанию можно опустить; обязательные должны быть переданы.

Сопоставление имён параметров

Как имя параметра привязывается к требованию:

  • Голое имя (target_db) сопоставляется с требованием с этим именем, принадлежащим монтируемому компоненту. Оно не пересекает границу требований другого модуля.
  • Квалифицированное имя (acme.jobs:target_db) сопоставляется ровно с этим id требования. Используйте его для устранения неоднозначности при проводке транзитивных зависимостей.

Если две зависимости передают разные значения для одного и того же требования — это конфликт, о котором сообщается (одинаковые значения допустимы).

Когда разрешаются значения

Внедрение происходит на стадии Link конвейера сборки — при публикации, при разворачивании зависимостей и при загрузке — а не во время выполнения. Стадия:

  1. Собирает каждое ns.requirement и каждое ns.dependency с его параметрами.
  2. Для каждого требования разрешает значение: побеждает подходящий параметр; иначе — значение по умолчанию; иначе (нет default) требование не разрешено.
  3. Записывает разрешённое значение в каждую целевую запись по её пути (установка или добавление для +=).

В режиме строгих требований неразрешённое обязательное требование проваливает сборку; иначе логируется предупреждение и работа продолжается. К моменту, когда записи достигают среды выполнения, каждое заполненное требование уже впечатано в свои цели.

Проверьте швы: тест монтирования

Юнит-тесты прогоняют слайс в изоляции; они не видят, целостен ли собранный модуль. Добавьте упаковочный тест (тест монтирования), который проверяет модуль целиком против живого реестра с внедрёнными требованиями:

  • каждый супервизируемый service указывает на существующую запись процесса,
  • каждый порождаемый или планируемый id разрешается в реальную запись,
  • хранилище каждой env.variable зарегистрировано.

Это интеграционные швы, которые маскируют изолированные юнит-наборы, — зазоры, из-за которых супервизор может ссылаться на воркер, который никогда не был зарегистрирован, или тестовая фикстура протащить id хранилища из тестовой обвязки в смонтированную загрузку. См. Супервизия и фреймворк Тестирование.

См. также