709 lines
24 KiB
Markdown
709 lines
24 KiB
Markdown
# Декомпозиция модулей
|
||
|
||
## 1. Цель документа
|
||
|
||
Этот документ фиксирует детальную структуру проекта до начала активной разработки. Его задача - заранее ограничить ответственность каждого компонента, избежать разрастания `crank-core`, не допустить появления "универсальных" структур на все случаи жизни и сохранить понятные границы между доменной логикой, runtime, адаптерами, API и UI.
|
||
|
||
Основной принцип: каждый crate отвечает за один слой системы. Внутри crate модули должны быть маленькими, тематическими и с минимальным количеством публичных сущностей.
|
||
|
||
## 2. Общие архитектурные правила
|
||
|
||
### 2.1. Что считается правильной декомпозицией
|
||
|
||
- `core` содержит только базовую доменную модель, идентификаторы, типы ошибок и общие контракты.
|
||
- `registry` отвечает только за хранение и загрузку конфигурации операций.
|
||
- `runtime` исполняет операции, но не знает о способе их хранения.
|
||
- адаптеры знают только свой протокол и общий контракт runtime.
|
||
- `admin-api` оркестрирует use case для UI, но не содержит протокольной логики.
|
||
- `mcp-server` публикует tools и вызывает runtime, но не содержит бизнес-логики конфигурирования.
|
||
- `ui` не знает внутреннюю реализацию runtime и работает только через HTTP API.
|
||
|
||
### 2.2. Что запрещено
|
||
|
||
- помещать SQL, HTTP-клиенты или gRPC-клиенты в `crank-core`;
|
||
- хранить в `core` "общие утилиты", не относящиеся к доменной модели;
|
||
- делать `runtime`, который напрямую читает БД;
|
||
- писать mapping-логику внутри REST, GraphQL или gRPC адаптеров;
|
||
- дублировать доменные типы в `admin-api`, `mcp-server` и адаптерах;
|
||
- создавать большие структуры вида `AppState`, в которые складывается все подряд;
|
||
- создавать большие enum или config-объекты, содержащие поля всех протоколов одновременно без выделенных вложенных типов.
|
||
|
||
### 2.3. Предпочтительный стиль
|
||
|
||
- узкие интерфейсы;
|
||
- маленькие DTO;
|
||
- отдельные типы для draft, published и runtime-view сущностей;
|
||
- отдельные модули для чтения, записи, валидации и исполнения;
|
||
- композиция из небольших сервисов вместо одного глобального сервиса.
|
||
|
||
## 3. Workspace-структура
|
||
|
||
Рекомендуемая структура:
|
||
|
||
```text
|
||
crank/
|
||
apps/
|
||
admin-api/
|
||
mcp-server/
|
||
ui/
|
||
crates/
|
||
crank-core/
|
||
crank-registry/
|
||
crank-runtime/
|
||
crank-adapter-rest/
|
||
crank-adapter-graphql/
|
||
crank-adapter-grpc/
|
||
crank-mapping/
|
||
crank-schema/
|
||
crank-proto/
|
||
```
|
||
|
||
Дополнительные crates `crank-mapping`, `crank-schema` и `crank-proto` нужны затем, чтобы не перегружать `crank-core`.
|
||
|
||
## 4. Детальная декомпозиция по crate
|
||
|
||
### 4.1. `crank-core`
|
||
|
||
Назначение:
|
||
|
||
- базовые доменные типы;
|
||
- идентификаторы;
|
||
- метаданные операций;
|
||
- общие контракты и ошибки верхнего уровня.
|
||
|
||
Что должно лежать в crate:
|
||
|
||
```text
|
||
crank-core/
|
||
src/
|
||
lib.rs
|
||
ids.rs
|
||
protocol.rs
|
||
operation/
|
||
mod.rs
|
||
model.rs
|
||
status.rs
|
||
target.rs
|
||
metadata.rs
|
||
auth/
|
||
mod.rs
|
||
profile.rs
|
||
secret_ref.rs
|
||
errors/
|
||
mod.rs
|
||
domain.rs
|
||
validation.rs
|
||
runtime.rs
|
||
```
|
||
|
||
Описание модулей:
|
||
|
||
- `ids.rs` - типы `OperationId`, `DescriptorId`, `ToolId` и другие идентификаторы.
|
||
- `protocol.rs` - enum протоколов и общие protocol capability flags.
|
||
- `operation/model.rs` - основная доменная модель операции без технических деталей хранения.
|
||
- `operation/status.rs` - типы состояний операции.
|
||
- `operation/target.rs` - базовые protocol-specific target structs.
|
||
- `operation/metadata.rs` - описание tool, display name, version, tags.
|
||
- `auth/profile.rs` - типы auth-профилей без привязки к конкретному клиенту.
|
||
- `auth/secret_ref.rs` - ссылки на секреты, а не сами секреты.
|
||
- `errors/*` - типизированные ошибки доменного слоя.
|
||
|
||
Что не должно лежать в crate:
|
||
|
||
- JSON Schema реализация;
|
||
- mapping engine;
|
||
- SQL-модели;
|
||
- HTTP DTO;
|
||
- protobuf parsing;
|
||
- `reqwest`, `sqlx`, `tonic`, `axum`.
|
||
|
||
Причина:
|
||
|
||
`crank-core` должен быть максимально стабильным и независимым. Если положить туда все подряд, он станет точкой связности всей системы.
|
||
|
||
### 4.2. `crank-schema`
|
||
|
||
Назначение:
|
||
|
||
- внутренняя модель схем;
|
||
- нормализация входа и выхода;
|
||
- представление полей для UI и runtime;
|
||
- преобразование схем из разных источников в единый вид.
|
||
|
||
Структура:
|
||
|
||
```text
|
||
crank-schema/
|
||
src/
|
||
lib.rs
|
||
schema/
|
||
mod.rs
|
||
model.rs
|
||
field.rs
|
||
scalar.rs
|
||
object.rs
|
||
collection.rs
|
||
oneof.rs
|
||
enums.rs
|
||
normalize/
|
||
mod.rs
|
||
json.rs
|
||
graphql.rs
|
||
protobuf.rs
|
||
validate/
|
||
mod.rs
|
||
input.rs
|
||
output.rs
|
||
```
|
||
|
||
Описание:
|
||
|
||
- `schema/model.rs` - корневая структура схемы.
|
||
- `field.rs` - описание поля, nullable, required, description.
|
||
- `scalar.rs` - базовые scalar types.
|
||
- `object.rs` - вложенные объекты.
|
||
- `collection.rs` - массивы и map-подобные структуры.
|
||
- `oneof.rs` - представление protobuf `oneof`.
|
||
- `enums.rs` - enum-значения и метаданные.
|
||
- `normalize/*` - преобразование GraphQL и protobuf моделей в единую схему.
|
||
- `normalize/json.rs` - нормализация загруженных JSON-примеров во внутреннюю schema model.
|
||
- `validate/*` - проверка JSON относительно внутренней схемы.
|
||
|
||
Почему отдельный crate:
|
||
|
||
Схемы будут использоваться почти везде, но это не повод тащить их в `core`. Иначе `core` станет тяжелым и начнет менять версию при каждом изменении схемной логики.
|
||
|
||
### 4.3. `crank-mapping`
|
||
|
||
Назначение:
|
||
|
||
- описание mapping DSL;
|
||
- компиляция mappings в runtime-представление;
|
||
- применение mappings к входу и выходу;
|
||
- автогенерация чернового mapping по загруженным примерам;
|
||
- трассировка ошибок маппинга.
|
||
|
||
Структура:
|
||
|
||
```text
|
||
crank-mapping/
|
||
src/
|
||
lib.rs
|
||
model/
|
||
mod.rs
|
||
mapping.rs
|
||
source.rs
|
||
target.rs
|
||
transform.rs
|
||
parser/
|
||
mod.rs
|
||
jsonpath.rs
|
||
compile/
|
||
mod.rs
|
||
plan.rs
|
||
infer/
|
||
mod.rs
|
||
from_samples.rs
|
||
from_schema.rs
|
||
execute/
|
||
mod.rs
|
||
input.rs
|
||
output.rs
|
||
errors.rs
|
||
```
|
||
|
||
Описание:
|
||
|
||
- `model/mapping.rs` - описание одного правила mapping.
|
||
- `model/source.rs` - откуда берем данные: `mcp`, `response`, `constant`.
|
||
- `model/target.rs` - куда кладем данные: `request.path`, `request.query`, `request.body`, `output`.
|
||
- `model/transform.rs` - ограниченный набор допустимых преобразований.
|
||
- `parser/jsonpath.rs` - единый parser и validator для `JSONPath` выражений.
|
||
- `compile/plan.rs` - предварительно скомпилированный план маппинга.
|
||
- `infer/from_samples.rs` - генерация чернового mapping по загруженным примерам JSON.
|
||
- `infer/from_schema.rs` - генерация чернового mapping по нормализованной схеме.
|
||
- `execute/input.rs` - применение mappings к запросу.
|
||
- `execute/output.rs` - применение mappings к ответу.
|
||
|
||
Антипаттерн, которого нужно избежать:
|
||
|
||
не помещать mapping-правила в строковые поля, которые потом интерпретируются каждым адаптером по-своему. Mapping должен быть единым движком.
|
||
|
||
### 4.4. `crank-proto`
|
||
|
||
Назначение:
|
||
|
||
- работа с `.proto` и descriptor set;
|
||
- извлечение services, methods и message schemas;
|
||
- преобразование protobuf metadata во внутренние типы.
|
||
|
||
Структура:
|
||
|
||
```text
|
||
crank-proto/
|
||
src/
|
||
lib.rs
|
||
descriptor/
|
||
mod.rs
|
||
loader.rs
|
||
source.rs
|
||
registry.rs
|
||
reflect/
|
||
mod.rs
|
||
client.rs
|
||
model/
|
||
mod.rs
|
||
service.rs
|
||
method.rs
|
||
message.rs
|
||
field.rs
|
||
convert/
|
||
mod.rs
|
||
to_schema.rs
|
||
to_json.rs
|
||
from_json.rs
|
||
errors.rs
|
||
```
|
||
|
||
Описание:
|
||
|
||
- `descriptor/loader.rs` - загрузка descriptor set.
|
||
- `descriptor/source.rs` - типы источников: upload, file, reflection.
|
||
- `descriptor/registry.rs` - индексирование описаний для поиска services/methods.
|
||
- `reflect/client.rs` - клиент server reflection, если будет добавлен.
|
||
- `model/*` - protobuf-ориентированная промежуточная модель.
|
||
- `convert/to_schema.rs` - перевод protobuf message в `crank-schema`.
|
||
- `convert/to_json.rs` и `from_json.rs` - преобразование runtime payload.
|
||
|
||
Почему отдельный crate:
|
||
|
||
protobuf-логика объемная и быстро начнет загрязнять gRPC adapter, если не отделить ее сразу.
|
||
|
||
### 4.5. `crank-registry`
|
||
|
||
Назначение:
|
||
|
||
- хранение операций, схем, descriptor links и статусов;
|
||
- выдача draft/published представлений;
|
||
- поиск активных операций для runtime и MCP server.
|
||
|
||
Структура:
|
||
|
||
```text
|
||
crank-registry/
|
||
src/
|
||
lib.rs
|
||
model/
|
||
mod.rs
|
||
record.rs
|
||
draft.rs
|
||
published.rs
|
||
repo/
|
||
mod.rs
|
||
operation_repo.rs
|
||
descriptor_repo.rs
|
||
service/
|
||
mod.rs
|
||
create_operation.rs
|
||
update_operation.rs
|
||
publish_operation.rs
|
||
list_operations.rs
|
||
get_runtime_view.rs
|
||
storage/
|
||
mod.rs
|
||
postgres.rs
|
||
cache/
|
||
mod.rs
|
||
runtime_cache.rs
|
||
errors.rs
|
||
```
|
||
|
||
Описание:
|
||
|
||
- `model/record.rs` - DB-aligned record model.
|
||
- `model/draft.rs` - модель черновика.
|
||
- `model/published.rs` - модель опубликованной операции.
|
||
- `repo/*` - контракты репозиториев.
|
||
- `service/*` - use case операции над реестром.
|
||
- `storage/*` - реализации репозиториев на `sqlx`.
|
||
- `cache/runtime_cache.rs` - кэш активных операций.
|
||
|
||
Правило:
|
||
|
||
`registry` не выполняет операции и не знает о `reqwest`/`tonic`. Он только хранит и отдает согласованные представления.
|
||
|
||
### 4.6. `crank-runtime`
|
||
|
||
Назначение:
|
||
|
||
- исполнение операций;
|
||
- orchestration между схемой, mapping и адаптерами;
|
||
- выдача нормализованного результата.
|
||
|
||
Структура:
|
||
|
||
```text
|
||
crank-runtime/
|
||
src/
|
||
lib.rs
|
||
executor/
|
||
mod.rs
|
||
operation_executor.rs
|
||
input_prepare.rs
|
||
output_finalize.rs
|
||
adapter/
|
||
mod.rs
|
||
traits.rs
|
||
dispatch.rs
|
||
context/
|
||
mod.rs
|
||
execution_context.rs
|
||
model/
|
||
mod.rs
|
||
runtime_operation.rs
|
||
prepared_request.rs
|
||
adapter_response.rs
|
||
errors.rs
|
||
```
|
||
|
||
Описание:
|
||
|
||
- `executor/operation_executor.rs` - основной orchestration use case.
|
||
- `executor/input_prepare.rs` - валидация входа и применение input mapping.
|
||
- `executor/output_finalize.rs` - обработка adapter response и output mapping.
|
||
- `adapter/traits.rs` - общий контракт для протокольных адаптеров.
|
||
- `adapter/dispatch.rs` - выбор адаптера по протоколу.
|
||
- `context/execution_context.rs` - correlation id, deadlines, tracing data.
|
||
- `model/runtime_operation.rs` - runtime-ready представление операции.
|
||
|
||
Правило:
|
||
|
||
`runtime` не должен знать, где хранится операция. Он получает уже готовую `runtime_operation`.
|
||
|
||
### 4.7. `crank-adapter-rest`
|
||
|
||
Назначение:
|
||
|
||
- построение и выполнение REST-вызовов.
|
||
|
||
Структура:
|
||
|
||
```text
|
||
crank-adapter-rest/
|
||
src/
|
||
lib.rs
|
||
client.rs
|
||
request/
|
||
mod.rs
|
||
build.rs
|
||
path.rs
|
||
query.rs
|
||
headers.rs
|
||
body.rs
|
||
response/
|
||
mod.rs
|
||
decode.rs
|
||
normalize.rs
|
||
errors.rs
|
||
```
|
||
|
||
Правило:
|
||
|
||
REST adapter не валидирует MCP input и не знает о registry. Он получает уже подготовленный request contract.
|
||
|
||
### 4.8. `crank-adapter-graphql`
|
||
|
||
Назначение:
|
||
|
||
- построение и выполнение GraphQL-вызовов.
|
||
|
||
Структура:
|
||
|
||
```text
|
||
crank-adapter-graphql/
|
||
src/
|
||
lib.rs
|
||
client.rs
|
||
request/
|
||
mod.rs
|
||
build.rs
|
||
variables.rs
|
||
response/
|
||
mod.rs
|
||
decode.rs
|
||
extract.rs
|
||
errors.rs
|
||
```
|
||
|
||
Правило:
|
||
|
||
GraphQL adapter не занимается introspection по умолчанию и не содержит редактор схем. Он только исполняет подготовленный operation template.
|
||
|
||
Дополнительное ограничение:
|
||
|
||
один GraphQL tool соответствует одному заранее определенному `query` или `mutation`. Адаптер не должен принимать от LLM произвольный GraphQL-документ, потому что в MCP-модели операция должна оставаться узкой, предсказуемой и валидируемой по фиксированной схеме.
|
||
|
||
### 4.9. `crank-adapter-grpc`
|
||
|
||
Назначение:
|
||
|
||
- выполнение unary gRPC-вызовов на основе уже выбранного метода и descriptor metadata.
|
||
|
||
Структура:
|
||
|
||
```text
|
||
crank-adapter-grpc/
|
||
src/
|
||
lib.rs
|
||
channel.rs
|
||
invoke/
|
||
mod.rs
|
||
unary.rs
|
||
request/
|
||
mod.rs
|
||
build.rs
|
||
response/
|
||
mod.rs
|
||
decode.rs
|
||
errors.rs
|
||
```
|
||
|
||
Правило:
|
||
|
||
gRPC adapter не должен сам парсить `.proto`. Этим занимается `crank-proto`. Иначе в адаптере смешаются discovery и execution.
|
||
|
||
Дополнительное ограничение:
|
||
|
||
adapter поддерживает только unary RPC. Streaming-вызовы не реализуются, потому что целевая модель MCP tool в проекте соответствует сценарию `запрос -> ответ`, а не долгоживущей сессии обмена сообщениями.
|
||
|
||
### 4.10. `admin-api`
|
||
|
||
Назначение:
|
||
|
||
- HTTP API для UI;
|
||
- координация use case создания, редактирования, тестирования и публикации.
|
||
|
||
Структура:
|
||
|
||
```text
|
||
apps/admin-api/
|
||
src/
|
||
main.rs
|
||
app.rs
|
||
state.rs
|
||
router.rs
|
||
http/
|
||
mod.rs
|
||
dto/
|
||
mod.rs
|
||
operation.rs
|
||
mapping.rs
|
||
test_run.rs
|
||
handlers/
|
||
mod.rs
|
||
create_operation.rs
|
||
update_operation.rs
|
||
publish_operation.rs
|
||
list_operations.rs
|
||
test_operation.rs
|
||
export_operation_yaml.rs
|
||
import_operation_yaml.rs
|
||
upload_json_samples.rs
|
||
upload_proto.rs
|
||
list_grpc_services.rs
|
||
response.rs
|
||
services/
|
||
mod.rs
|
||
operation_service.rs
|
||
descriptor_service.rs
|
||
config_portability_service.rs
|
||
test_service.rs
|
||
errors.rs
|
||
```
|
||
|
||
Правила:
|
||
|
||
- HTTP DTO не должны протекать в доменный слой;
|
||
- handlers должны быть тонкими;
|
||
- orchestration должна жить в `services/*`;
|
||
- `state.rs` не должен разрастаться в огромную структуру. Лучше использовать вложенные state-компоненты или отдельные service bundles.
|
||
- import/export конфигурации в `YAML` должен быть отдельным use case, а не побочным эффектом обычного CRUD.
|
||
|
||
### 4.11. `mcp-server`
|
||
|
||
Назначение:
|
||
|
||
- публикация MCP tools;
|
||
- вызов runtime по имени tool;
|
||
- обновление активного списка tools.
|
||
|
||
Структура:
|
||
|
||
```text
|
||
apps/mcp-server/
|
||
src/
|
||
main.rs
|
||
app.rs
|
||
state.rs
|
||
tools/
|
||
mod.rs
|
||
list.rs
|
||
call.rs
|
||
cache.rs
|
||
translate/
|
||
mod.rs
|
||
to_mcp_tool.rs
|
||
from_mcp_input.rs
|
||
to_mcp_output.rs
|
||
errors.rs
|
||
```
|
||
|
||
Правило:
|
||
|
||
`mcp-server` не должен реализовывать business rules публикации. Он читает уже опубликованные операции и транслирует их в MCP.
|
||
|
||
### 4.12. `ui`
|
||
|
||
Назначение:
|
||
|
||
- интерфейс оператора.
|
||
|
||
Предлагаемая frontend-структура:
|
||
|
||
```text
|
||
apps/ui/
|
||
src/
|
||
main.tsx
|
||
app/
|
||
router.tsx
|
||
providers.tsx
|
||
pages/
|
||
operation-list/
|
||
operation-create/
|
||
operation-edit/
|
||
operation-test/
|
||
grpc-browser/
|
||
features/
|
||
operation-form/
|
||
mapping-editor/
|
||
sample-upload/
|
||
grpc-method-picker/
|
||
schema-viewer/
|
||
publish-operation/
|
||
entities/
|
||
operation/
|
||
descriptor/
|
||
shared/
|
||
api/
|
||
lib/
|
||
ui/
|
||
config/
|
||
```
|
||
|
||
Правило:
|
||
|
||
UI должен декомпозироваться по пользовательским сценариям, а не по типам файлов уровня "все компоненты в одной папке".
|
||
|
||
## 5. Правила зависимостей между crate
|
||
|
||
Целевой граф зависимостей:
|
||
|
||
```text
|
||
crank-core
|
||
crank-schema -> crank-core
|
||
crank-mapping -> crank-core
|
||
crank-proto -> crank-core, crank-schema
|
||
crank-registry -> crank-core, crank-schema, crank-mapping
|
||
crank-adapter-rest -> crank-core
|
||
crank-adapter-graphql -> crank-core
|
||
crank-adapter-grpc -> crank-core, crank-proto
|
||
crank-runtime -> crank-core, crank-schema, crank-mapping, adapters
|
||
admin-api -> crank-core, crank-schema, crank-mapping, crank-proto, crank-registry, crank-runtime
|
||
mcp-server -> crank-core, crank-registry, crank-runtime
|
||
```
|
||
|
||
Критические ограничения:
|
||
|
||
- `crank-core` ни от кого не зависит;
|
||
- адаптеры не зависят от `registry`;
|
||
- `runtime` не зависит от `admin-api` и `mcp-server`;
|
||
- `registry` не зависит от адаптеров;
|
||
- `ui` зависит только от HTTP API.
|
||
|
||
## 6. Границы публичных API модулей
|
||
|
||
Чтобы структура не разъехалась, нужно заранее ограничить публичность.
|
||
|
||
Рекомендуемое правило:
|
||
|
||
- наружу экспортируются только корневые доменные типы, service-интерфейсы и ошибки;
|
||
- внутренние DTO, record-модели и промежуточные builder-структуры остаются `pub(crate)`;
|
||
- не реэкспортировать целые деревья модулей без необходимости;
|
||
- не делать `mod utils`, если можно назвать модуль по смыслу.
|
||
|
||
Пример плохого решения:
|
||
|
||
- `pub mod common;`
|
||
- `pub mod helpers;`
|
||
- `pub struct AppContext { ... 25 полей ... }`
|
||
|
||
Пример правильного решения:
|
||
|
||
- `pub struct OperationExecutor`
|
||
- `pub trait OperationRepository`
|
||
- `pub struct RuntimeOperation`
|
||
|
||
## 7. Какие большие структуры точно не нужны
|
||
|
||
Ниже список сущностей, которые легко превращаются в антипаттерн:
|
||
|
||
- одна гигантская `Operation`, содержащая сразу все REST, GraphQL и gRPC поля;
|
||
- один `MappingConfig`, содержащий и input, и output, и transforms, и validation rules без разделения;
|
||
- единый `AppState` со всеми репозиториями, клиентами, кэшами и конфигами;
|
||
- один `ProtocolAdapter` с ветвлением `match protocol` внутри на сотни строк;
|
||
- один `SchemaField` без выделения object/array/enum/oneof вариантов.
|
||
|
||
Правильный подход:
|
||
|
||
- отдельные target-типы по протоколам;
|
||
- отдельные input/output mapping модели;
|
||
- отдельные bounded state-наборы для каждого приложения;
|
||
- отдельные adapter crates;
|
||
- выделенная иерархия schema types.
|
||
|
||
## 8. Порядок реализации без архитектурного долга
|
||
|
||
Рекомендуемый порядок разработки:
|
||
|
||
1. `crank-core`
|
||
2. `crank-schema`
|
||
3. `crank-mapping`
|
||
4. `crank-registry`
|
||
5. `crank-adapter-rest`
|
||
6. `crank-runtime`
|
||
7. `admin-api`
|
||
8. `ui`
|
||
9. `crank-proto`
|
||
10. `crank-adapter-grpc`
|
||
11. `crank-adapter-graphql`
|
||
12. `mcp-server`
|
||
|
||
Причина такого порядка:
|
||
|
||
- сначала фиксируется доменная модель;
|
||
- затем схема и mapping как самые чувствительные части;
|
||
- затем реестр и базовое выполнение REST;
|
||
- после этого можно собирать UI и только потом наращивать сложные протоколы.
|
||
|
||
## 9. Практический итог
|
||
|
||
Если придерживаться этой декомпозиции, то:
|
||
|
||
- `crank-core` останется маленьким и стабильным;
|
||
- schema и mapping не смешаются с transport-логикой;
|
||
- protobuf discovery не загрязнит gRPC runtime;
|
||
- `admin-api` и `mcp-server` останутся тонкими входными слоями;
|
||
- добавление нового протокола не потребует переписывать половину проекта.
|
||
|
||
Это и есть целевая архитектурная дисциплина проекта: отдельные слои, отдельные модели, минимально необходимая публичность и отсутствие "универсальных" структур, в которые со временем начинает стекаться вся система.
|