Skip to content
This repository has been archived by the owner on Apr 10, 2024. It is now read-only.

Commit

Permalink
Cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
seowalex committed Dec 11, 2022
1 parent aaf0d5d commit c86fae4
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 110 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ humantime = "2.1.0"
indexmap = { version = "1.9.2", features = ["serde"] }
itertools = "0.10.5"
nom = "7.1.1"
once_cell = "1.16.0"
parse-hyperlinks = "0.23.4"
serde = { version = "1.0.149", features = ["derive"] }
serde_ignored = "0.1.5"
Expand Down
29 changes: 15 additions & 14 deletions src/compose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,18 @@ use std::{env, fs};
use yansi::Paint;

use crate::Config;
use parser::{State, Token, Var};
use types::Compose;

fn evaluate(tokens: Vec<parser::Token>) -> Result<String> {
fn evaluate(tokens: Vec<Token>) -> Result<String> {
tokens
.into_iter()
.map(|token| match token {
parser::Token::Str(string) => Ok(string),
parser::Token::Var(name, var) => match var {
Some(parser::Var::Default(state, tokens)) => match state {
parser::State::Set => env::var(name),
parser::State::SetAndNonEmpty => env::var(name).and_then(|var| {
Token::Str(string) => Ok(string),
Token::Var(name, var) => match var {
Some(Var::Default(state, tokens)) => match state {
State::Set => env::var(name),
State::SetAndNonEmpty => env::var(name).and_then(|var| {
if var.is_empty() {
Err(env::VarError::NotPresent)
} else {
Expand All @@ -28,9 +29,9 @@ fn evaluate(tokens: Vec<parser::Token>) -> Result<String> {
}),
}
.or_else(|_| evaluate(tokens)),
Some(parser::Var::Err(state, tokens)) => match state {
parser::State::Set => env::var(&name),
parser::State::SetAndNonEmpty => env::var(&name).and_then(|var| {
Some(Var::Err(state, tokens)) => match state {
State::Set => env::var(&name),
State::SetAndNonEmpty => env::var(&name).and_then(|var| {
if var.is_empty() {
Err(env::VarError::NotPresent)
} else {
Expand All @@ -47,10 +48,10 @@ fn evaluate(tokens: Vec<parser::Token>) -> Result<String> {
}
})
}),
Some(parser::Var::Replace(state, tokens)) => {
Some(Var::Replace(state, tokens)) => {
if match state {
parser::State::Set => env::var(name),
parser::State::SetAndNonEmpty => env::var(name).and_then(|var| {
State::Set => env::var(name),
State::SetAndNonEmpty => env::var(name).and_then(|var| {
if var.is_empty() {
Err(env::VarError::NotPresent)
} else {
Expand Down Expand Up @@ -88,8 +89,8 @@ fn interpolate(mut value: Value) -> Result<Value> {
}
} else if let Some(values) = value.as_mapping_mut() {
for (key, value) in values.into_iter() {
*value = interpolate(value.to_owned())
.with_context(|| key.as_str().unwrap_or_default().to_owned())?;
*value =
interpolate(value.to_owned()).with_context(|| key.as_str().unwrap().to_owned())?;
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/compose/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ fn parameter_expanded(input: &str) -> IResult<&str, Token> {
"?" => Some(Var::Err(State::Set, tokens)),
":+" => Some(Var::Replace(State::SetAndNonEmpty, tokens)),
"+" => Some(Var::Replace(State::Set, tokens)),
_ => None,
_ => unreachable!(),
},
)
},
Expand Down
177 changes: 177 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
use anyhow::Result;
use clap::Args;
use figment::{
providers::{Env, Serialized},
Figment,
};
use itertools::iproduct;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use serde_with::{
formats::{CommaSeparator, Separator},
serde_as, skip_serializing_none, BoolFromInt, PickFirst, StringWithSeparator,
};
use std::{
env, fs,
io::{Error, ErrorKind},
path::{Path, PathBuf},
};

static COMPOSE_FILE_NAMES: Lazy<Vec<String>> = Lazy::new(|| {
iproduct!(["compose", "docker-compose"], ["yaml", "yml"])
.map(|name| format!("{}.{}", name.0, name.1))
.collect()
});

pub(crate) struct PathSeparator;

impl Separator for PathSeparator {
fn separator() -> &'static str {
Box::leak(
env::var("COMPOSE_PATH_SEPARATOR")
.unwrap_or_else(|_| {
String::from(if cfg!(unix) {
":"
} else if cfg!(windows) {
";"
} else {
unreachable!()
})
})
.into_boxed_str(),
)
}
}

#[skip_serializing_none]
#[serde_as]
#[derive(Args, Serialize, Deserialize, Debug)]
pub(crate) struct Config {
/// Project name
#[arg(short, long)]
pub(crate) project_name: Option<String>,

/// Compose configuration files
#[arg(short, long)]
#[serde_as(as = "Option<PickFirst<(_, StringWithSeparator::<PathSeparator, String>)>>")]
pub(crate) file: Option<Vec<String>>,

/// Specify a profile to enable
#[arg(long)]
#[serde_as(as = "Option<PickFirst<(_, StringWithSeparator::<CommaSeparator, String>)>>")]
#[serde(rename = "profiles")]
pub(crate) profile: Option<Vec<String>>,

/// Specify an alternate environment file
#[arg(long)]
pub(crate) env_file: Option<String>,

/// Specify an alternate working directory
#[arg(long)]
pub(crate) project_directory: Option<String>,

#[arg(skip)]
#[serde_as(as = "Option<PickFirst<(_, BoolFromInt)>>")]
pub(crate) convert_windows_paths: Option<bool>,

#[arg(skip)]
pub(crate) path_separator: Option<String>,

#[arg(skip)]
#[serde_as(as = "Option<PickFirst<(_, BoolFromInt)>>")]
pub(crate) ignore_orphans: Option<bool>,
}

fn find(directory: &Path, files: &Vec<String>) -> Result<PathBuf> {
let paths = files
.iter()
.map(|file| directory.join(file))
.collect::<Vec<_>>();

for path in paths {
if path.is_file() {
return Ok(path);
}
}

if let Some(parent) = directory.parent() {
find(parent, files)
} else {
Err(Error::new(
ErrorKind::NotFound,
"Compose file not found in the working directory or its parent directories",
))?
}
}

fn resolve(config: &Config) -> Result<Config> {
let mut config = Figment::new()
.merge(Env::prefixed("COMPOSE_").ignore(&["env_file", "project_directory"]))
.merge(Serialized::defaults(config))
.extract::<Config>()?;
let file = find(env::current_dir()?.as_path(), &COMPOSE_FILE_NAMES)?;
println!("2");

for file in config.file.get_or_insert_with(|| {
let override_file = file.with_extension(format!(
"override.{}",
file.extension().unwrap().to_string_lossy()
));

if override_file.is_file() {
vec![&file, &override_file]
} else {
vec![&file]
}
.into_iter()
.map(|file| file.to_string_lossy().to_string())
.collect()
}) {
*file = fs::canonicalize(&file)?.to_string_lossy().to_string();
}

if let Some(file) = config
.file
.get_or_insert_with(|| {
let override_file = file.with_extension(format!(
"override.{}",
file.extension().unwrap().to_string_lossy()
));

if override_file.is_file() {
vec![&file, &override_file]
} else {
vec![&file]
}
.into_iter()
.map(|file| file.to_string_lossy().to_string())
.collect()
})
.first()
{
config.project_directory.get_or_insert_with(|| {
Path::new(file)
.parent()
.map(|parent| parent.to_string_lossy().to_string())
.unwrap_or_default()
});
}

config.env_file.get_or_insert_with(|| {
config
.project_directory
.to_owned()
.map(PathBuf::from)
.unwrap_or_default()
.join(".env")
.to_string_lossy()
.to_string()
});

Ok(config)
}

pub(crate) fn parse(config: &Config) -> Result<Config> {
dotenvy::from_filename(resolve(config)?.env_file.unwrap()).ok();
resolve(config)
}
100 changes: 5 additions & 95 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,11 @@
mod commands;
mod compose;
mod config;

use anyhow::Result;
use clap::Parser;
use figment::{
providers::{Env, Serialized},
Figment,
};
use serde::{Deserialize, Serialize};
use serde_with::{
formats::{CommaSeparator, Separator},
serde_as, skip_serializing_none, BoolFromInt, PickFirst, StringWithSeparator,
};
use std::{
env,
path::{Path, PathBuf},
};

use config::Config;

#[derive(Parser, Debug)]
#[command(version, about, next_display_order = None)]
Expand All @@ -27,91 +17,11 @@ struct Args {
config: Config,
}

struct PathSeparator;

impl Separator for PathSeparator {
fn separator() -> &'static str {
Box::leak(
env::var("COMPOSE_PATH_SEPARATOR")
.unwrap_or_else(|_| String::from(":"))
.into_boxed_str(),
)
}
}

#[skip_serializing_none]
#[serde_as]
#[derive(clap::Args, Serialize, Deserialize, Debug)]
struct Config {
/// Project name
#[arg(short, long)]
project_name: Option<String>,

/// Compose configuration files
#[arg(short, long)]
#[serde_as(as = "Option<PickFirst<(_, StringWithSeparator::<PathSeparator, String>)>>")]
file: Option<Vec<String>>,

/// Specify a profile to enable
#[arg(long = "profile")]
#[serde_as(as = "Option<PickFirst<(_, StringWithSeparator::<CommaSeparator, String>)>>")]
profiles: Option<Vec<String>>,

/// Specify an alternate environment file
#[arg(long)]
env_file: Option<String>,

/// Specify an alternate working directory
#[arg(long)]
project_directory: Option<String>,

#[arg(skip)]
#[serde_as(as = "Option<PickFirst<(_, BoolFromInt)>>")]
convert_windows_paths: Option<bool>,

#[arg(skip)]
path_separator: Option<String>,

#[arg(skip)]
#[serde_as(as = "Option<PickFirst<(_, BoolFromInt)>>")]
ignore_orphans: Option<bool>,
}

fn main() -> Result<()> {
let args = Args::parse();
let mut config = Figment::new()
.merge(Env::prefixed("COMPOSE_"))
.merge(Serialized::defaults(&args.config))
.extract::<Config>()?;

if let Some(file) = &config.file {
config.project_directory = file.first().and_then(|file| {
Path::new(file)
.parent()
.and_then(|dir| dir.to_str().map(String::from))
});
}

dotenvy::from_filename(
config
.env_file
.to_owned()
.map(PathBuf::from)
.unwrap_or_else(|| {
config
.project_directory
.to_owned()
.map(PathBuf::from)
.unwrap_or_default()
.join(Path::new(".env"))
}),
)
.ok();
let config = config::parse(&args.config)?;

let config = Figment::new()
.merge(Env::prefixed("COMPOSE_"))
.merge(Serialized::defaults(args.config))
.extract::<Config>()?;
println!("{:#?}", config);

commands::run(args.command, config)?;

Expand Down

0 comments on commit c86fae4

Please sign in to comment.