refac: move commands to separate folder
This commit is contained in:
parent
20a520b662
commit
40c0a43dab
10 changed files with 242 additions and 181 deletions
28
migra-cli/src/commands/apply.rs
Normal file
28
migra-cli/src/commands/apply.rs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::database;
|
||||||
|
use crate::opts::ApplyCommandOpt;
|
||||||
|
use crate::path::PathBuilder;
|
||||||
|
use crate::StdResult;
|
||||||
|
|
||||||
|
pub(crate) fn apply_sql(config: Config, opts: ApplyCommandOpt) -> StdResult<()> {
|
||||||
|
let database_connection_string = &config.database_connection_string()?;
|
||||||
|
let mut client = database::connect(database_connection_string)?;
|
||||||
|
|
||||||
|
let file_path = PathBuilder::from(config.directory_path())
|
||||||
|
.append(opts.file_name)
|
||||||
|
.default_extension("sql")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let content = std::fs::read_to_string(file_path)?;
|
||||||
|
|
||||||
|
match database::apply_sql(&mut client, &content) {
|
||||||
|
Ok(_) => {
|
||||||
|
println!("File was applied successfully");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
println!("{}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
23
migra-cli/src/commands/downgrade.rs
Normal file
23
migra-cli/src/commands/downgrade.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::database;
|
||||||
|
use crate::StdResult;
|
||||||
|
|
||||||
|
pub(crate) fn downgrade_applied_migrations(config: Config) -> StdResult<()> {
|
||||||
|
let database_connection_string = &config.database_connection_string()?;
|
||||||
|
let mut client = database::connect(database_connection_string)?;
|
||||||
|
|
||||||
|
let applied_migrations = database::applied_migrations(&mut client)?;
|
||||||
|
let migrations = config.migrations()?;
|
||||||
|
|
||||||
|
if let Some(first_applied_migration) = applied_migrations.first() {
|
||||||
|
if let Some(migration) = migrations
|
||||||
|
.iter()
|
||||||
|
.find(|m| m.name() == first_applied_migration)
|
||||||
|
{
|
||||||
|
println!("downgrade {}...", migration.name());
|
||||||
|
migration.downgrade(&mut client)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
34
migra-cli/src/commands/init.rs
Normal file
34
migra-cli/src/commands/init.rs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::config::MIGRA_TOML_FILENAME;
|
||||||
|
use crate::StdResult;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub(crate) fn initialize_migra_manifest(config_path: Option<PathBuf>) -> StdResult<()> {
|
||||||
|
let config_path = config_path
|
||||||
|
.map(|mut config_path| {
|
||||||
|
let ext = config_path.extension();
|
||||||
|
if config_path.is_dir() || ext.is_none() {
|
||||||
|
config_path.push(MIGRA_TOML_FILENAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
config_path
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| PathBuf::from(MIGRA_TOML_FILENAME));
|
||||||
|
|
||||||
|
if config_path.exists() {
|
||||||
|
println!("{} already exists", config_path.to_str().unwrap());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(dirs) = config_path.parent() {
|
||||||
|
std::fs::create_dir_all(dirs)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = Config::default();
|
||||||
|
let content = toml::to_string(&config)?;
|
||||||
|
std::fs::write(&config_path, content)?;
|
||||||
|
|
||||||
|
println!("Created {}", config_path.to_str().unwrap());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
51
migra-cli/src/commands/list.rs
Normal file
51
migra-cli/src/commands/list.rs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::database;
|
||||||
|
use crate::error::{ErrorKind, StdResult};
|
||||||
|
|
||||||
|
const EM_DASH: char = '—';
|
||||||
|
|
||||||
|
pub(crate) fn print_migration_lists(config: Config) -> StdResult<()> {
|
||||||
|
let applied_migrations = match config.database_connection_string() {
|
||||||
|
Ok(ref database_connection_string) => {
|
||||||
|
let mut client = database::connect(database_connection_string)?;
|
||||||
|
let applied_migrations = database::applied_migrations(&mut client)?;
|
||||||
|
|
||||||
|
println!("Applied migrations:");
|
||||||
|
if applied_migrations.is_empty() {
|
||||||
|
println!("{}", EM_DASH);
|
||||||
|
} else {
|
||||||
|
applied_migrations
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.for_each(|name| println!("{}", name));
|
||||||
|
}
|
||||||
|
|
||||||
|
applied_migrations
|
||||||
|
}
|
||||||
|
Err(e) if *e.kind() == ErrorKind::MissedEnvVar(String::new()) => {
|
||||||
|
println!("{}", e.kind());
|
||||||
|
println!("No connection to database");
|
||||||
|
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
Err(e) => panic!(e),
|
||||||
|
};
|
||||||
|
|
||||||
|
println!();
|
||||||
|
|
||||||
|
let pending_migrations = config
|
||||||
|
.migrations()?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|m| !applied_migrations.contains(m.name()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
println!("Pending migrations:");
|
||||||
|
if pending_migrations.is_empty() {
|
||||||
|
println!("{}", EM_DASH);
|
||||||
|
} else {
|
||||||
|
pending_migrations.iter().for_each(|m| {
|
||||||
|
println!("{}", m.name());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
45
migra-cli/src/commands/make.rs
Normal file
45
migra-cli/src/commands/make.rs
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
use crate::opts::MakeCommandOpt;
|
||||||
|
use crate::path::PathBuilder;
|
||||||
|
use crate::Config;
|
||||||
|
use crate::StdResult;
|
||||||
|
use chrono::Local;
|
||||||
|
|
||||||
|
pub(crate) fn make_migration(config: Config, opts: MakeCommandOpt) -> StdResult<()> {
|
||||||
|
let now = Local::now().format("%y%m%d%H%M%S");
|
||||||
|
|
||||||
|
let migration_name: String = opts
|
||||||
|
.migration_name
|
||||||
|
.to_lowercase()
|
||||||
|
.chars()
|
||||||
|
.map(|c| match c {
|
||||||
|
'0'..='9' | 'a'..='z' => c,
|
||||||
|
_ => '_',
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let migration_dir_path = PathBuilder::from(config.migration_dir_path())
|
||||||
|
.append(format!("{}_{}", now, migration_name))
|
||||||
|
.build();
|
||||||
|
if !migration_dir_path.exists() {
|
||||||
|
std::fs::create_dir_all(&migration_dir_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let upgrade_migration_path = PathBuilder::from(&migration_dir_path)
|
||||||
|
.append("up.sql")
|
||||||
|
.build();
|
||||||
|
if !upgrade_migration_path.exists() {
|
||||||
|
std::fs::write(upgrade_migration_path, "-- Your SQL goes here\n\n")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let downgrade_migration_path = PathBuilder::from(&migration_dir_path)
|
||||||
|
.append("down.sql")
|
||||||
|
.build();
|
||||||
|
if !downgrade_migration_path.exists() {
|
||||||
|
std::fs::write(
|
||||||
|
downgrade_migration_path,
|
||||||
|
"-- This file should undo anything in `up.sql`\n\n",
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
13
migra-cli/src/commands/mod.rs
Normal file
13
migra-cli/src/commands/mod.rs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
mod apply;
|
||||||
|
mod downgrade;
|
||||||
|
mod init;
|
||||||
|
mod list;
|
||||||
|
mod make;
|
||||||
|
mod upgrade;
|
||||||
|
|
||||||
|
pub(crate) use apply::*;
|
||||||
|
pub(crate) use downgrade::*;
|
||||||
|
pub(crate) use init::*;
|
||||||
|
pub(crate) use list::*;
|
||||||
|
pub(crate) use make::*;
|
||||||
|
pub(crate) use upgrade::*;
|
26
migra-cli/src/commands/upgrade.rs
Normal file
26
migra-cli/src/commands/upgrade.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
use crate::database;
|
||||||
|
use crate::Config;
|
||||||
|
use crate::StdResult;
|
||||||
|
|
||||||
|
pub(crate) fn upgrade_pending_migrations(config: Config) -> StdResult<()> {
|
||||||
|
let database_connection_string = &config.database_connection_string()?;
|
||||||
|
let mut client = database::connect(database_connection_string)?;
|
||||||
|
|
||||||
|
let applied_migrations = database::applied_migrations(&mut client)?;
|
||||||
|
|
||||||
|
let migrations = config.migrations()?;
|
||||||
|
|
||||||
|
if migrations.is_empty() || migrations.last().map(|m| m.name()) == applied_migrations.first() {
|
||||||
|
println!("Up to date");
|
||||||
|
} else {
|
||||||
|
for m in migrations
|
||||||
|
.iter()
|
||||||
|
.filter(|m| !applied_migrations.contains(m.name()))
|
||||||
|
{
|
||||||
|
println!("upgrade {}...", m.name());
|
||||||
|
m.upgrade(&mut client)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -5,15 +5,15 @@ use serde::{Deserialize, Serialize};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::{env, fs, io};
|
use std::{env, fs, io};
|
||||||
|
|
||||||
const MIGRA_TOML_FILENAME: &str = "Migra.toml";
|
pub(crate) const MIGRA_TOML_FILENAME: &str = "Migra.toml";
|
||||||
const DEFAULT_DATABASE_CONNECTION_ENV: &str = "$DATABASE_URL";
|
pub(crate) const DEFAULT_DATABASE_CONNECTION_ENV: &str = "$DATABASE_URL";
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub(crate) struct Config {
|
pub(crate) struct Config {
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
root: PathBuf,
|
manifest_root: PathBuf,
|
||||||
|
|
||||||
directory: PathBuf,
|
root: PathBuf,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
database: DatabaseConfig,
|
database: DatabaseConfig,
|
||||||
|
@ -27,8 +27,8 @@ pub(crate) struct DatabaseConfig {
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
fn default() -> Config {
|
fn default() -> Config {
|
||||||
Config {
|
Config {
|
||||||
root: PathBuf::new(),
|
manifest_root: PathBuf::new(),
|
||||||
directory: PathBuf::from("database"),
|
root: PathBuf::from("database"),
|
||||||
database: DatabaseConfig {
|
database: DatabaseConfig {
|
||||||
connection: Some(String::from(DEFAULT_DATABASE_CONNECTION_ENV)),
|
connection: Some(String::from(DEFAULT_DATABASE_CONNECTION_ENV)),
|
||||||
},
|
},
|
||||||
|
@ -73,7 +73,7 @@ impl Config {
|
||||||
let content = fs::read_to_string(&config_path)?;
|
let content = fs::read_to_string(&config_path)?;
|
||||||
|
|
||||||
let mut config: Config = toml::from_str(&content).expect("Cannot parse Migra.toml");
|
let mut config: Config = toml::from_str(&content).expect("Cannot parse Migra.toml");
|
||||||
config.root = config_path
|
config.manifest_root = config_path
|
||||||
.parent()
|
.parent()
|
||||||
.unwrap_or_else(|| Path::new(""))
|
.unwrap_or_else(|| Path::new(""))
|
||||||
.to_path_buf();
|
.to_path_buf();
|
||||||
|
@ -82,40 +82,12 @@ impl Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn initialize(config_path: Option<PathBuf>) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let config_path = config_path
|
|
||||||
.map(|mut config_path| {
|
|
||||||
let ext = config_path.extension();
|
|
||||||
if config_path.is_dir() || ext.is_none() {
|
|
||||||
config_path.push(MIGRA_TOML_FILENAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
config_path
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| PathBuf::from(MIGRA_TOML_FILENAME));
|
|
||||||
|
|
||||||
if config_path.exists() {
|
|
||||||
println!("{} already exists", config_path.to_str().unwrap());
|
|
||||||
return Ok(());
|
|
||||||
} else if let Some(dirs) = config_path.parent() {
|
|
||||||
fs::create_dir_all(dirs)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let config = Config::default();
|
|
||||||
let content = toml::to_string(&config)?;
|
|
||||||
fs::write(&config_path, content)?;
|
|
||||||
|
|
||||||
println!("Created {}", config_path.to_str().unwrap());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn directory_path(&self) -> PathBuf {
|
pub fn directory_path(&self) -> PathBuf {
|
||||||
PathBuilder::from(&self.root)
|
PathBuilder::from(&self.manifest_root)
|
||||||
.append(&self.directory)
|
.append(&self.root)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ use std::fmt;
|
||||||
use std::mem;
|
use std::mem;
|
||||||
use std::result;
|
use std::result;
|
||||||
|
|
||||||
|
pub type StdResult<T> = result::Result<T, Box<dyn std::error::Error>>;
|
||||||
pub type Result<T> = result::Result<T, Error>;
|
pub type Result<T> = result::Result<T, Error>;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#![deny(clippy::all)]
|
#![deny(clippy::all)]
|
||||||
|
|
||||||
|
mod commands;
|
||||||
mod config;
|
mod config;
|
||||||
mod database;
|
mod database;
|
||||||
mod error;
|
mod error;
|
||||||
|
@ -7,169 +8,36 @@ mod migration;
|
||||||
mod opts;
|
mod opts;
|
||||||
mod path;
|
mod path;
|
||||||
|
|
||||||
use chrono::Local;
|
use crate::error::StdResult;
|
||||||
use config::Config;
|
use config::Config;
|
||||||
use error::ErrorKind;
|
use opts::{AppOpt, Command, StructOpt};
|
||||||
use opts::{AppOpt, ApplyCommandOpt, Command, MakeCommandOpt, StructOpt};
|
|
||||||
use path::PathBuilder;
|
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
const EM_DASH: char = '—';
|
fn main() -> StdResult<()> {
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let opt = AppOpt::from_args();
|
let opt = AppOpt::from_args();
|
||||||
|
|
||||||
match opt.command {
|
match opt.command {
|
||||||
Command::Init => {
|
Command::Init => {
|
||||||
Config::initialize(opt.config)?;
|
commands::initialize_migra_manifest(opt.config)?;
|
||||||
}
|
}
|
||||||
Command::Apply(ApplyCommandOpt { file_name }) => {
|
Command::Apply(opts) => {
|
||||||
let config = Config::read(opt.config)?;
|
let config = Config::read(opt.config)?;
|
||||||
|
commands::apply_sql(config, opts)?;
|
||||||
let database_connection_string = &config.database_connection_string()?;
|
|
||||||
let mut client = database::connect(database_connection_string)?;
|
|
||||||
|
|
||||||
let file_path = PathBuilder::from(config.directory_path())
|
|
||||||
.append(file_name)
|
|
||||||
.default_extension("sql")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let content = fs::read_to_string(file_path)?;
|
|
||||||
|
|
||||||
match database::apply_sql(&mut client, &content) {
|
|
||||||
Ok(_) => {
|
|
||||||
println!("File was applied successfully")
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
println!("{}", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Command::Make(MakeCommandOpt { migration_name }) => {
|
Command::Make(opts) => {
|
||||||
let config = Config::read(opt.config)?;
|
let config = Config::read(opt.config)?;
|
||||||
|
commands::make_migration(config, opts)?;
|
||||||
let now = Local::now().format("%y%m%d%H%M%S");
|
|
||||||
|
|
||||||
let migration_name: String = migration_name
|
|
||||||
.to_lowercase()
|
|
||||||
.chars()
|
|
||||||
.map(|c| match c {
|
|
||||||
'0'..='9' | 'a'..='z' => c,
|
|
||||||
_ => '_',
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let migration_dir_path = PathBuilder::from(config.migration_dir_path())
|
|
||||||
.append(format!("{}_{}", now, migration_name))
|
|
||||||
.build();
|
|
||||||
if !migration_dir_path.exists() {
|
|
||||||
fs::create_dir_all(&migration_dir_path)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let upgrade_migration_path = PathBuilder::from(&migration_dir_path)
|
|
||||||
.append("up.sql")
|
|
||||||
.build();
|
|
||||||
if !upgrade_migration_path.exists() {
|
|
||||||
fs::write(upgrade_migration_path, "-- Your SQL goes here\n\n")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let downgrade_migration_path = PathBuilder::from(&migration_dir_path)
|
|
||||||
.append("down.sql")
|
|
||||||
.build();
|
|
||||||
if !downgrade_migration_path.exists() {
|
|
||||||
fs::write(
|
|
||||||
downgrade_migration_path,
|
|
||||||
"-- This file should undo anything in `up.sql`\n\n",
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Command::List => {
|
Command::List => {
|
||||||
let config = Config::read(opt.config)?;
|
let config = Config::read(opt.config)?;
|
||||||
|
commands::print_migration_lists(config)?;
|
||||||
let applied_migrations = match config.database_connection_string() {
|
|
||||||
Ok(ref database_connection_string) => {
|
|
||||||
let mut client = database::connect(database_connection_string)?;
|
|
||||||
let applied_migrations = database::applied_migrations(&mut client)?;
|
|
||||||
|
|
||||||
println!("Applied migrations:");
|
|
||||||
if applied_migrations.is_empty() {
|
|
||||||
println!("{}", EM_DASH);
|
|
||||||
} else {
|
|
||||||
applied_migrations
|
|
||||||
.iter()
|
|
||||||
.rev()
|
|
||||||
.for_each(|name| println!("{}", name));
|
|
||||||
}
|
|
||||||
|
|
||||||
applied_migrations
|
|
||||||
}
|
|
||||||
Err(e) if *e.kind() == ErrorKind::MissedEnvVar(String::new()) => {
|
|
||||||
println!("{}", e.kind());
|
|
||||||
println!("No connection to database");
|
|
||||||
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
Err(e) => panic!(e),
|
|
||||||
};
|
|
||||||
|
|
||||||
println!();
|
|
||||||
|
|
||||||
let pending_migrations = config
|
|
||||||
.migrations()?
|
|
||||||
.into_iter()
|
|
||||||
.filter(|m| !applied_migrations.contains(m.name()))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
println!("Pending migrations:");
|
|
||||||
if pending_migrations.is_empty() {
|
|
||||||
println!("{}", EM_DASH);
|
|
||||||
} else {
|
|
||||||
pending_migrations.iter().for_each(|m| {
|
|
||||||
println!("{}", m.name());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Command::Upgrade => {
|
Command::Upgrade => {
|
||||||
let config = Config::read(opt.config)?;
|
let config = Config::read(opt.config)?;
|
||||||
|
commands::upgrade_pending_migrations(config)?;
|
||||||
let database_connection_string = &config.database_connection_string()?;
|
|
||||||
let mut client = database::connect(database_connection_string)?;
|
|
||||||
|
|
||||||
let applied_migrations = database::applied_migrations(&mut client)?;
|
|
||||||
|
|
||||||
let migrations = config.migrations()?;
|
|
||||||
|
|
||||||
if migrations.is_empty()
|
|
||||||
|| migrations.last().map(|m| m.name()) == applied_migrations.first()
|
|
||||||
{
|
|
||||||
println!("Up to date");
|
|
||||||
} else {
|
|
||||||
for m in migrations
|
|
||||||
.iter()
|
|
||||||
.filter(|m| !applied_migrations.contains(m.name()))
|
|
||||||
{
|
|
||||||
println!("upgrade {}...", m.name());
|
|
||||||
m.upgrade(&mut client)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Command::Downgrade => {
|
Command::Downgrade => {
|
||||||
let config = Config::read(opt.config)?;
|
let config = Config::read(opt.config)?;
|
||||||
|
commands::downgrade_applied_migrations(config)?;
|
||||||
let database_connection_string = &config.database_connection_string()?;
|
|
||||||
let mut client = database::connect(database_connection_string)?;
|
|
||||||
|
|
||||||
let applied_migrations = database::applied_migrations(&mut client)?;
|
|
||||||
let migrations = config.migrations()?;
|
|
||||||
|
|
||||||
if let Some(first_applied_migration) = applied_migrations.first() {
|
|
||||||
if let Some(migration) = migrations
|
|
||||||
.iter()
|
|
||||||
.find(|m| m.name() == first_applied_migration)
|
|
||||||
{
|
|
||||||
println!("downgrade {}...", migration.name());
|
|
||||||
migration.downgrade(&mut client)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Reference in a new issue