use anyhow::anyhow;
use clap::{Args, ValueEnum};
use copybook_core::{Feature, FeatureCategory, FeatureFlags};
use std::path::PathBuf;
use std::str::FromStr;
#[derive(Args, Debug, Clone)]
pub(crate) struct FeatureFlagOpts {
#[arg(long, value_delimiter = ',', value_name = "FEATURE")]
pub enable_features: Vec<String>,
#[arg(long, value_delimiter = ',', value_name = "FEATURE")]
pub disable_features: Vec<String>,
#[arg(long, value_name = "CATEGORY")]
pub enable_category: Vec<String>,
#[arg(long, value_name = "CATEGORY")]
pub disable_category: Vec<String>,
#[arg(long, value_name = "PATH")]
pub feature_flags_config: Option<PathBuf>,
#[arg(long)]
pub list_features: bool,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
pub(crate) enum DialectPreference {
#[value(name = "n")]
N,
#[value(name = "0")]
Zero,
#[value(name = "1")]
One,
}
impl From<DialectPreference> for copybook_core::dialect::Dialect {
#[inline]
fn from(value: DialectPreference) -> Self {
match value {
DialectPreference::N => Self::Normative,
DialectPreference::Zero => Self::ZeroTolerant,
DialectPreference::One => Self::OneTolerant,
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
pub(crate) enum ZonedEncodingPreference {
#[value(alias = "preferred-zero")]
Preferred,
Ascii,
Ebcdic,
Auto,
}
impl From<ZonedEncodingPreference> for copybook_codec::ZonedEncodingFormat {
#[inline]
fn from(value: ZonedEncodingPreference) -> Self {
match value {
ZonedEncodingPreference::Preferred | ZonedEncodingPreference::Auto => Self::Auto,
ZonedEncodingPreference::Ascii => Self::Ascii,
ZonedEncodingPreference::Ebcdic => Self::Ebcdic,
}
}
}
#[allow(clippy::too_many_lines)]
pub(crate) fn initialize_feature_flags(opts: &FeatureFlagOpts) -> anyhow::Result<FeatureFlags> {
use std::fs;
use std::io::Read;
let mut flags = FeatureFlags::from_env();
if let Some(config_path) = &opts.feature_flags_config {
let mut content = String::new();
let mut file = fs::File::open(config_path)
.map_err(|e| anyhow!("Failed to open feature flags config: {e}"))?;
file.read_to_string(&mut content)
.map_err(|e| anyhow!("Failed to read feature flags config: {e}"))?;
if let Ok(json_config) = serde_json::from_str::<serde_json::Value>(&content) {
apply_json_feature_flags(&mut flags, &json_config);
} else if let Ok(toml_str) = content.parse::<toml::Value>() {
apply_toml_feature_flags(&mut flags, &toml_str);
} else {
return Err(anyhow!(
"Failed to parse feature flags config: expected JSON or TOML format"
));
}
}
apply_category_flags(&mut flags, &opts.enable_category, FeatureFlagAction::Enable)?;
apply_category_flags(
&mut flags,
&opts.disable_category,
FeatureFlagAction::Disable,
)?;
apply_named_flags(&mut flags, &opts.enable_features, FeatureFlagAction::Enable)?;
apply_named_flags(
&mut flags,
&opts.disable_features,
FeatureFlagAction::Disable,
)?;
Ok(flags)
}
fn apply_json_feature_flags(flags: &mut FeatureFlags, config: &serde_json::Value) {
if let Some(feature_flags) = config.get("feature_flags") {
if let Some(enabled) = feature_flags.get("enabled").and_then(|v| v.as_array()) {
for feature_name in enabled {
if let Some(name) = feature_name.as_str()
&& let Ok(feature) = Feature::from_str(name)
{
flags.enable(feature);
}
}
}
if let Some(disabled) = feature_flags.get("disabled").and_then(|v| v.as_array()) {
for feature_name in disabled {
if let Some(name) = feature_name.as_str()
&& let Ok(feature) = Feature::from_str(name)
{
flags.disable(feature);
}
}
}
}
}
fn apply_toml_feature_flags(flags: &mut FeatureFlags, config: &toml::Value) {
if let Some(feature_flags) = config.get("feature_flags") {
if let Some(enabled) = feature_flags.get("enabled").and_then(|v| v.as_array()) {
for feature_name in enabled {
if let Some(name) = feature_name.as_str()
&& let Ok(feature) = Feature::from_str(name)
{
flags.enable(feature);
}
}
}
if let Some(disabled) = feature_flags.get("disabled").and_then(|v| v.as_array()) {
for feature_name in disabled {
if let Some(name) = feature_name.as_str()
&& let Ok(feature) = Feature::from_str(name)
{
flags.disable(feature);
}
}
}
}
}
#[derive(Copy, Clone)]
enum FeatureFlagAction {
Enable,
Disable,
}
impl FeatureFlagAction {
fn apply(self, flags: &mut FeatureFlags, feature: Feature) {
match self {
Self::Enable => flags.enable(feature),
Self::Disable => flags.disable(feature),
}
}
}
fn apply_category_flags(
flags: &mut FeatureFlags,
category_names: &[String],
action: FeatureFlagAction,
) -> anyhow::Result<()> {
for category_name in category_names {
let category = parse_feature_category(category_name)?;
for feature in FeatureFlags::features_in_category(category) {
action.apply(flags, feature);
}
}
Ok(())
}
fn parse_feature_category(category_name: &str) -> anyhow::Result<FeatureCategory> {
match category_name.to_lowercase().as_str() {
"experimental" => Ok(FeatureCategory::Experimental),
"enterprise" => Ok(FeatureCategory::Enterprise),
"performance" => Ok(FeatureCategory::Performance),
"debug" => Ok(FeatureCategory::Debug),
"testing" => Ok(FeatureCategory::Testing),
_ => Err(anyhow!(
"Invalid feature category '{category_name}'. Valid categories: experimental, enterprise, performance, debug, testing"
)),
}
}
fn apply_named_flags(
flags: &mut FeatureFlags,
feature_names: &[String],
action: FeatureFlagAction,
) -> anyhow::Result<()> {
for feature_name in feature_names {
let feature = Feature::from_str(feature_name)
.map_err(|_| anyhow!("Invalid feature flag '{feature_name}'"))?;
action.apply(flags, feature);
}
Ok(())
}
#[allow(clippy::unwrap_used)]
pub(crate) fn list_all_features(flags: &FeatureFlags) {
use std::io::Write;
let stdout = std::io::stdout();
let mut stdout = stdout.lock();
writeln!(stdout, "Available Feature Flags:").unwrap();
writeln!(stdout).unwrap();
for category in [
FeatureCategory::Experimental,
FeatureCategory::Enterprise,
FeatureCategory::Performance,
FeatureCategory::Debug,
FeatureCategory::Testing,
] {
writeln!(stdout, "{}:", category.to_string().to_uppercase()).unwrap();
for feature in FeatureFlags::features_in_category(category) {
let status = if flags.is_enabled(feature) {
"enabled"
} else {
"disabled"
};
writeln!(
stdout,
" {:20} ({:8}) - {}",
feature.to_string(),
status,
feature.description()
)
.unwrap();
}
writeln!(stdout).unwrap();
}
writeln!(
stdout,
"Environment variables: COPYBOOK_FF_<FEATURE_NAME>=1 to enable"
)
.unwrap();
}
pub(crate) fn effective_dialect(cli_dialect: Option<DialectPreference>) -> DialectPreference {
if let Some(dialect) = cli_dialect {
return dialect;
}
if let Ok(env_val) = std::env::var("COPYBOOK_DIALECT") {
match env_val.trim().to_ascii_lowercase().as_str() {
"0" => DialectPreference::Zero,
"1" => DialectPreference::One,
_ => DialectPreference::N, }
} else {
DialectPreference::N }
}