# 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; async fn create_version(&self, cmd: CreateVersion) -> Result; 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 { repo: R, } impl PublishOperationService 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`-блоки по проекту ### В `mcpaas-core` - маленькие `impl` на domain types; - status helpers; - derived metadata methods. ### В `mcpaas-schema` - schema validation; - field traversal; - shape helpers. ### В `mcpaas-mapping` - JSONPath validation; - mapping rule helpers; - execution helpers. ### В `mcpaas-proto` - metadata conversion helpers; - descriptor lookup helpers. ### В `mcpaas-registry` - service methods, а не методы на доменных структурах; - repository implementations. ### В `mcpaas-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` с разнородной логикой.