/
codenik
/
codenik-tools
Обзор
Документация
Войти
/
codenik
/
codenik-tools
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
main
crates/codenik-cli/src/factory.rs
188 строк
7 KB
Codenik Wizard
feat(cli): add doctor command for config and provider diagnostics
09 май 2026, 00:59
09 май 2026, 00:59
d99ec4f
Код
Авторство
О чём код?
//! Factory для создания провайдеров по их имени. //! //! Токены берутся через [`codenik_storage::CredentialStore`]. На текущем этапе //! используется цепочка из двух [`codenik_storage::EnvStore`]: один без //! префикса (видит `GITVERSE_TOKEN`), второй с префиксом `CODENIK_` (видит //! `CODENIK_GITVERSE_TOKEN`). После реализации auth login сюда добавится //! `KeyringStore`. use std::sync::Arc; use anyhow::{Context, Result, anyhow}; use codenik_core::{IssueProvider, MergeRequestProvider, RepoProvider}; use codenik_storage::{ChainStore, CredentialStore, EnvStore, KeyringStore}; use codenik_yandex_tracker::OrgKind; use secrecy::SecretString; pub fn default_credential_store() -> ChainStore { ChainStore::new() .with_store(Box::new(EnvStore::with_prefix(""))) .with_store(Box::new(EnvStore::new())) .with_store(Box::new(KeyringStore::new())) } /// Получить токен для провайдера или вернуть подсказывающую ошибку. pub fn require_token( store: &dyn CredentialStore, keyring_key: &str, env_hint: &str, ) -> Result<SecretString> { let token = store .get(keyring_key) .with_context(|| format!("ошибка чтения хранилища секретов для `{keyring_key}`"))?; token.ok_or_else(|| { let provider_cli_name = keyring_key .split('.') .next() .unwrap_or(keyring_key) .replace('_', "-"); anyhow!( "токен `{keyring_key}` не найден.\n\ Установите переменную окружения `{env_hint}` или выполните\n\ `codenik auth login {provider_cli_name}`." ) }) } /// Создать `RepoProvider` по имени провайдера. pub fn make_repo_provider( name: &str, store: &dyn CredentialStore, ) -> Result<Arc<dyn RepoProvider>> { match name { "gitverse" => { let client = build_gitverse(store)?; Ok(Arc::new(client)) } "sourcecraft" => { let client = build_sourcecraft(store)?; Ok(Arc::new(client)) } "yandex-tracker" => Err(anyhow!( "yandex-tracker не имеет git-репозиториев — `repos` команды для него не применимы" )), other => Err(anyhow!( "неизвестный provider `{other}`. Доступные: gitverse, sourcecraft" )), } } /// Создать `IssueProvider` по имени провайдера. pub fn make_issue_provider( name: &str, store: &dyn CredentialStore, ) -> Result<Arc<dyn IssueProvider>> { match name { "gitverse" => Ok(Arc::new(build_gitverse(store)?)), "sourcecraft" => Ok(Arc::new(build_sourcecraft(store)?)), "yandex-tracker" => Ok(Arc::new(build_yandex_tracker(store)?)), other => Err(anyhow!( "неизвестный provider `{other}`. Доступные: gitverse, sourcecraft, yandex-tracker" )), } } /// Создать `MergeRequestProvider` по имени провайдера. pub fn make_merge_request_provider( name: &str, store: &dyn CredentialStore, ) -> Result<Arc<dyn MergeRequestProvider>> { match name { "gitverse" => Ok(Arc::new(build_gitverse(store)?)), "sourcecraft" => Ok(Arc::new(build_sourcecraft(store)?)), "yandex-tracker" => Err(anyhow!( "yandex-tracker не имеет merge / pull request'ов — `mrs` команды для него не применимы" )), other => Err(anyhow!( "неизвестный provider `{other}`. Доступные: gitverse, sourcecraft" )), } } pub(crate) fn build_gitverse( store: &dyn CredentialStore, ) -> Result<codenik_gitverse::GitVerseClient> { let token = require_token(store, "gitverse.token", "GITVERSE_TOKEN")?; let base = std::env::var("GITVERSE_API_BASE").ok(); let client = match base { Some(b) => codenik_gitverse::GitVerseClient::with_base_url(token, b)?, None => codenik_gitverse::GitVerseClient::new(token)?, }; Ok(client) } pub(crate) fn build_sourcecraft( store: &dyn CredentialStore, ) -> Result<codenik_sourcecraft::SourceCraftClient> { let token = require_token(store, "sourcecraft.token", "SOURCECRAFT_TOKEN")?; let base = std::env::var("SOURCECRAFT_API_BASE").ok(); let client = match base { Some(b) => codenik_sourcecraft::SourceCraftClient::with_base_url(token, b)?, None => codenik_sourcecraft::SourceCraftClient::new(token)?, }; Ok(client) } pub(crate) fn build_yandex_tracker( store: &dyn CredentialStore, ) -> Result<codenik_yandex_tracker::YandexTrackerClient> { let token = require_token(store, "yandex_tracker.token", "YANDEX_OAUTH_TOKEN")?; let org = match ( std::env::var("YANDEX_TRACKER_ORG_ID").ok().filter(|s| !s.is_empty()), std::env::var("YANDEX_TRACKER_CLOUD_ORG_ID").ok().filter(|s| !s.is_empty()), ) { (Some(o), _) => OrgKind::Yandex360(o), (_, Some(o)) => OrgKind::YandexCloud(o), _ => { return Err(anyhow!( "для Yandex Tracker задайте одну из переменных окружения: \ `YANDEX_TRACKER_ORG_ID` (Yandex 360) или `YANDEX_TRACKER_CLOUD_ORG_ID` (Yandex Cloud)" )); } }; let base = std::env::var("YANDEX_TRACKER_API_BASE").ok(); let client = match base { Some(b) => codenik_yandex_tracker::YandexTrackerClient::with_base_url(token, org, b)?, None => codenik_yandex_tracker::YandexTrackerClient::new(token, org)?, }; Ok(client) } #[cfg(test)] mod tests { use super::*; fn expect_err(r: Result<Arc<dyn RepoProvider>>) -> anyhow::Error { match r { Ok(_) => panic!("expected error"), Err(e) => e, } } #[test] fn unknown_provider_returns_error() { let store = default_credential_store(); let err = expect_err(make_repo_provider("nope", &store)); assert!(err.to_string().contains("неизвестный provider")); } #[test] fn yandex_tracker_is_not_a_repo_provider() { let store = default_credential_store(); let err = expect_err(make_repo_provider("yandex-tracker", &store)); assert!(err.to_string().contains("git-репозиториев")); } #[test] fn missing_token_reports_env_hint() { // SAFETY: tests run single-threaded per scope. unsafe { std::env::remove_var("GITVERSE_TOKEN"); std::env::remove_var("CODENIK_GITVERSE_TOKEN"); } let store = default_credential_store(); let err = expect_err(make_repo_provider("gitverse", &store)); let s = err.to_string(); assert!(s.contains("GITVERSE_TOKEN")); } }