# Rust Design

## 1. Назначение документа

Этот документ фиксирует, как проектировать поведение в Rust-коде:

- какие методы допустимы на `struct` и `enum`;
- что должно жить в `impl`;
- что должно быть вынесено в `trait`;
- что должно быть оформлено как `service` или `use case`.

Главная цель документа - не допустить появления `god-struct`, когда одна сущность одновременно:

- хранит данные;
- валидирует себя целиком;
- ходит в БД;
- дергает HTTP;
- строит mapping;
- управляет publish flow;
- содержит половину бизнес-логики проекта.

## 2. Базовое правило

В Rust нужно разделять:

- `data model`
- `domain behavior`
- `integration contracts`
- `application services`

То есть:

- `struct` и `enum` хранят состояние;
- `impl` на них содержит только локально связанное поведение;
- `trait` задает внешний контракт;
- `service` и `use case` координируют несколько сущностей и внешние зависимости.

## 3. Что допустимо держать в `impl` на структурах

На `impl` допустимы только методы, которые:

- опираются только на внутреннее состояние структуры;
- не требуют инфраструктурных зависимостей;
- не ходят в БД;
- не выполняют сетевые вызовы;
- не меняют чужие aggregate boundaries.

Подходящие примеры:

- `Operation::is_published()`
- `Operation::supports_protocol(protocol)`
- `Operation::tool_name()`
- `MappingRule::is_required()`
- `GeneratedDraft::is_available()`
- `Schema::field(path)`
- `Protocol::as_str()`

Неподходящие примеры:

- `Operation::save(db)`
- `Operation::publish(repo, runtime, cache)`
- `Operation::call_external_api()`
- `Operation::load_descriptor()`

## 4. Какие методы должны жить на ключевых доменных структурах

### 4.1. `Operation`

Допустимые методы:

- `fn tool_name(&self) -> &str`
- `fn is_draft(&self) -> bool`
- `fn is_published(&self) -> bool`
- `fn protocol(&self) -> &Protocol`
- `fn auth_profile_ref(&self) -> Option<&str>`
- `fn can_be_published(&self) -> bool`

Что не должно жить здесь:

- создание новой версии;
- publish;
- YAML import/export;
- DB persistence;
- runtime execution;
- adapter dispatch.

### 4.2. `Target`

Допустимые методы:

- `fn kind(&self) -> Protocol`
- `fn summary(&self) -> String`

Что не должно жить здесь:

- реальный вызов REST/GraphQL/gRPC;
- загрузка descriptor set;
- introspection;
- network logic.

### 4.3. `Schema`

Допустимые методы:

- `fn is_object(&self) -> bool`
- `fn field(&self, name: &str) -> Option<&SchemaField>`
- `fn has_required_fields(&self) -> bool`
- `fn validate_shape(&self, value: &serde_json::Value) -> Result<(), SchemaError>`

Что не должно жить здесь:

- UI rendering logic;
- DB serialization logic;
- adapter-specific request assembly.

### 4.4. `MappingSet` и `MappingRule`

Допустимые методы:

- `fn is_empty(&self) -> bool`
- `fn validate_paths(&self) -> Result<(), MappingError>`
- `fn target_context(&self) -> MappingTargetContext`

Что не должно жить здесь:

- protocol adapter branching;
- network execution;
- persistence;
- доступ к registry.

### 4.5. `ExecutionConfig`

Допустимые методы:

- `fn timeout(&self) -> Duration`
- `fn has_auth(&self) -> bool`
- `fn protocol_options(&self) -> Option<&ProtocolOptions>`

Что не должно жить здесь:

- secret resolution;
- создание HTTP headers из env;
- динамическое чтение конфигов приложения.

## 5. Что нужно выносить в `trait`

`Trait` нужен там, где появляется внешний контракт, который имеет несколько реализаций или зависит от инфраструктуры.

Правильные кандидаты:

- `OperationRepository`
- `PublishedOperationRepository`
- `ProtocolAdapter`
- `SecretResolver`
- `DescriptorStore`
- `ArtifactStore`
- `YamlCodec`
- `DraftGenerator`

### Пример

```rust
pub trait OperationRepository {
    async fn get(&self, id: &OperationId) -> Result<OperationRecord, RepoError>;
    async fn create_version(&self, cmd: CreateVersion) -> Result<OperationVersionRef, RepoError>;
    async fn publish(&self, id: &OperationId, version: u32) -> Result<(), RepoError>;
}
```

Почему это `trait`, а не метод на `Operation`:

- потому что операция сама не должна знать, как она хранится;
- потому что хранение - инфраструктурная зависимость;
- потому что это boundary между доменом и storage.

## 6. Что нужно выносить в service/use-case слой

Если логика:

- координирует несколько сущностей;
- использует `trait`-зависимости;
- меняет состояние нескольких aggregate boundaries;
- имеет бизнес-шаги;

то это `service`, а не `impl` на структуре.

Кандидаты:

- `CreateOperationService`
- `CreateOperationVersionService`
- `PublishOperationService`
- `GenerateDraftService`
- `TestOperationService`
- `ImportOperationYamlService`
- `ExportOperationYamlService`
- `ListPublishedToolsService`
- `OperationExecutor`

## 7. Рекомендуемое распределение поведения

### Domain `impl`

Хранит:

- локальную валидацию;
- derived methods;
- простые status checks;
- инварианты одной сущности.

### `trait`

Хранит:

- внешние контракты;
- infrastructure boundaries;
- replaceable dependencies.

### `service`

Хранит:

- orchestration;
- use case sequence;
- transaction boundaries;
- вызовы нескольких зависимостей.

## 8. Пример правильного разделения

### Плохо

```rust
impl Operation {
    pub async fn publish(
        &mut self,
        repo: &SqlOperationRepository,
        cache: &RuntimeCache,
        secret_resolver: &EnvSecretResolver,
    ) -> Result<(), Error> {
        self.validate()?;
        repo.save(self).await?;
        cache.reload().await?;
        let _ = secret_resolver.resolve(...)?;
        self.status = Status::Published;
        Ok(())
    }
}
```

Почему плохо:

- доменная сущность знает про SQL;
- знает про кэш;
- знает про secret resolver;
- меняет себя и внешний мир одновременно;
- содержит orchestration.

### Правильно

```rust
impl Operation {
    pub fn can_be_published(&self) -> bool {
        matches!(self.status, Status::Draft | Status::Testing)
            && !self.input_mapping.rules.is_empty()
            && !self.output_mapping.rules.is_empty()
    }
}

pub struct PublishOperationService<R> {
    repo: R,
}

impl<R> PublishOperationService<R>
where
    R: OperationRepository,
{
    pub async fn execute(
        &self,
        operation_id: &OperationId,
        version: u32,
    ) -> Result<(), PublishError> {
        let op = self.repo.get_version(operation_id, version).await?;
        if !op.can_be_published() {
            return Err(PublishError::InvalidState);
        }
        self.repo.publish(operation_id, version).await
    }
}
```

## 9. Признаки `god-struct`

Если у структуры:

- слишком много полей из разных bounded contexts;
- методы и на schema, и на DB, и на adapters, и на YAML;
- методы с кучей зависимостей в аргументах;
- методы длиннее, чем небольшой локальный инвариант;
- много `match protocol` прямо внутри доменной модели;

то это уже `god-struct`.

Особенно опасные кандидаты:

- `Operation`
- `AppState`
- `OperationExecutor`
- `AdminService`
- `ProtocolAdapter`

## 10. Как не допустить `god-struct`

### 10.1. Для `Operation`

Не добавлять туда:

- repo methods;
- transport methods;
- import/export;
- publish flow;
- sample upload handling.

### 10.2. Для `AppState`

Не складывать все зависимости в один плоский объект на 20 полей.

Лучше:

- `RegistryServices`
- `RuntimeServices`
- `ArtifactServices`
- `AuthServices`

### 10.3. Для `OperationExecutor`

Он может быть orchestration root, но не должен становиться монолитом.

Нужно выделять:

- `InputPrepare`
- `AdapterDispatch`
- `OutputFinalize`
- `ExecutionContextFactory`

## 11. Рекомендуемые `impl`-блоки по проекту

### В `crank-core`

- маленькие `impl` на domain types;
- status helpers;
- derived metadata methods.

### В `crank-schema`

- schema validation;
- field traversal;
- shape helpers.

### В `crank-mapping`

- JSONPath validation;
- mapping rule helpers;
- execution helpers.

### В `crank-proto`

- metadata conversion helpers;
- descriptor lookup helpers.

### В `crank-registry`

- service methods, а не методы на доменных структурах;
- repository implementations.

### В `crank-runtime`

- orchestration services;
- adapter dispatch;
- runtime context management.

## 12. Что лучше описывать не как методы структур

Следующие вещи лучше описывать отдельными сервисами даже если технически их можно записать как `impl`:

- `publish`
- `create_version`
- `import_yaml`
- `export_yaml`
- `generate_draft`
- `test_run`
- `reload_published_tools`

## 13. Практический итог

Для этого проекта хорошее правило такое:

- `struct` знает только себя;
- `trait` знает границу;
- `service` знает сценарий;
- `adapter` знает протокол;
- `repository` знает storage.

Если придерживаться этой схемы, то Rust-код останется модульным, а `Operation` и связанные типы не превратятся в `god-struct` с разнородной логикой.
