use std::io::Write;
use std::path::PathBuf;
use std::process;
use std::time::{SystemTime, UNIX_EPOCH};
use clap::{Parser, Subcommand};
use serde_json::json;
#[derive(Parser)]
#[command(name = "variable")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Generate {
#[arg(long)]
out: PathBuf,
file: PathBuf,
},
Encode {
#[arg(long, default_value_t = 0)]
schema_revision: u64,
#[arg(long, default_value_t = 0)]
manifest_revision: u64,
#[arg(long)]
generated_at_unix_ms: Option<u64>,
#[arg(long)]
source: Option<String>,
file: PathBuf,
},
Decode {
#[arg(long)]
pretty: bool,
#[arg(long)]
fail_on_error: bool,
#[arg(long)]
schema: Option<PathBuf>,
file: PathBuf,
},
}
fn main() {
let cli = Cli::parse();
match cli.command {
Commands::Generate { out, file } => {
if let Err(e) = generate(&file, &out) {
eprintln!("{}", e);
process::exit(1);
}
}
Commands::Encode {
schema_revision,
manifest_revision,
generated_at_unix_ms,
source,
file,
} => {
let generated_at_unix_ms = match generated_at_unix_ms {
Some(value) => value,
None => match current_unix_ms() {
Ok(value) => value,
Err(e) => {
eprintln!("{}", e);
process::exit(1);
}
},
};
let metadata = variable_wire::SnapshotMetadata {
schema_revision,
manifest_revision,
generated_at_unix_ms,
source,
};
if let Err(e) = encode(&file, metadata) {
eprintln!("{}", e);
process::exit(1);
}
}
Commands::Decode {
pretty,
fail_on_error,
schema,
file,
} => {
if let Err(e) = decode(&file, pretty, fail_on_error, schema.as_ref()) {
eprintln!("{}", e);
process::exit(1);
}
}
}
}
fn parse_var_file(file: &PathBuf) -> Result<variable_core::ast::VarFile, String> {
let source = std::fs::read_to_string(file)
.map_err(|e| format!("error reading {}: {}", file.display(), e))?;
variable_core::parse_and_validate(&source).map_err(|e| match e {
variable_core::Error::Lex(e) => {
format!(
"Lex error at line {}, column {}: {}",
e.span.line, e.span.column, e.message
)
}
variable_core::Error::Parse(e) => {
format!(
"Parse error at line {}, column {}: {}",
e.span.line, e.span.column, e.message
)
}
variable_core::Error::Validation(errors) => {
let msgs: Vec<String> = errors.iter().map(|e| format!("{}", e)).collect();
msgs.join("\n")
}
})
}
fn generate(file: &PathBuf, out_dir: &PathBuf) -> Result<(), String> {
let var_file = parse_var_file(file)?;
let ts_output = variable_codegen::generate_typescript(&var_file);
std::fs::create_dir_all(out_dir)
.map_err(|e| format!("error creating directory {}: {}", out_dir.display(), e))?;
let stem = file
.file_stem()
.ok_or_else(|| format!("cannot determine file name from {}", file.display()))?;
let out_file = out_dir.join(format!("{}.generated.ts", stem.to_string_lossy()));
std::fs::write(&out_file, ts_output)
.map_err(|e| format!("error writing {}: {}", out_file.display(), e))?;
println!("Generated {}", out_file.display());
Ok(())
}
fn encode(file: &PathBuf, metadata: variable_wire::SnapshotMetadata) -> Result<(), String> {
let var_file = parse_var_file(file)?;
let bytes = variable_wire::encode_var_file_defaults(&var_file, metadata)
.map_err(|e| format!("encode error: {}", e))?;
let mut stdout = std::io::stdout().lock();
stdout
.write_all(&bytes)
.map_err(|e| format!("error writing binary output: {}", e))?;
stdout
.flush()
.map_err(|e| format!("error flushing binary output: {}", e))?;
Ok(())
}
fn current_unix_ms() -> Result<u64, String> {
let duration = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| format!("error getting current system time: {}", e))?;
Ok(duration.as_millis() as u64)
}
fn decode(
file: &PathBuf,
pretty: bool,
fail_on_error: bool,
schema: Option<&PathBuf>,
) -> Result<(), String> {
let bytes =
std::fs::read(file).map_err(|e| format!("error reading {}: {}", file.display(), e))?;
let var_file = match schema {
Some(schema_path) => Some(parse_var_file(schema_path)?),
None => None,
};
let report = variable_wire::decode_snapshot(&bytes);
let has_error = report
.diagnostics
.iter()
.any(|diagnostic| diagnostic.severity == variable_wire::DiagnosticSeverity::Error);
let json_output = decode_report_to_json(&report, var_file.as_ref());
let rendered = if pretty {
serde_json::to_string_pretty(&json_output)
.map_err(|e| format!("error serializing decode output: {}", e))?
} else {
serde_json::to_string(&json_output)
.map_err(|e| format!("error serializing decode output: {}", e))?
};
println!("{}", rendered);
if fail_on_error && has_error {
return Err("decode reported error diagnostics; see JSON output".to_string());
}
Ok(())
}
fn decode_report_to_json(
report: &variable_wire::DecodeReport,
_schema: Option<&variable_core::ast::VarFile>,
) -> serde_json::Value {
let snapshot_json = report.snapshot.as_ref().map(|snapshot| {
json!({
"metadata": {
"schema_revision": snapshot.metadata.schema_revision,
"manifest_revision": snapshot.metadata.manifest_revision,
"generated_at_unix_ms": snapshot.metadata.generated_at_unix_ms,
"source": snapshot.metadata.source,
},
"features": snapshot.features.iter().map(|feature| {
json!({
"feature_id": feature.feature_id,
"variables": feature.variables.iter().map(|variable| {
json!({
"variable_id": variable.variable_id,
"value": value_to_json(&variable.value),
})
}).collect::<Vec<_>>(),
})
}).collect::<Vec<_>>(),
})
});
let diagnostics_json = report
.diagnostics
.iter()
.map(|diagnostic| {
json!({
"kind": diagnostic_kind_str(diagnostic.kind),
"severity": diagnostic_severity_str(diagnostic.severity),
"message": diagnostic.message,
})
})
.collect::<Vec<_>>();
json!({
"snapshot": snapshot_json,
"diagnostics": diagnostics_json,
})
}
fn value_to_json(value: &variable_core::ast::Value) -> serde_json::Value {
match value {
variable_core::ast::Value::Boolean(v) => json!({
"type": "boolean",
"value": v,
}),
variable_core::ast::Value::Integer(v) => json!({
"type": "integer",
"value": v,
}),
variable_core::ast::Value::Float(v) => json!({
"type": "float",
"value": v,
}),
variable_core::ast::Value::String(v) => json!({
"type": "string",
"value": v,
}),
variable_core::ast::Value::Struct {
struct_name,
fields,
} => {
let fields_json: serde_json::Map<std::string::String, serde_json::Value> = fields
.iter()
.map(|(k, v)| (k.clone(), value_to_json(v)))
.collect();
json!({
"type": "struct",
"struct_name": struct_name,
"fields": fields_json,
})
}
}
}
fn diagnostic_kind_str(kind: variable_wire::DiagnosticKind) -> &'static str {
match kind {
variable_wire::DiagnosticKind::UnsupportedVersion => "unsupported_version",
variable_wire::DiagnosticKind::MalformedEnvelope => "malformed_envelope",
variable_wire::DiagnosticKind::TruncatedSection => "truncated_section",
variable_wire::DiagnosticKind::LimitExceeded => "limit_exceeded",
variable_wire::DiagnosticKind::UnknownSectionType => "unknown_section_type",
variable_wire::DiagnosticKind::DuplicateFeatureId => "duplicate_feature_id",
variable_wire::DiagnosticKind::DuplicateVariableId => "duplicate_variable_id",
variable_wire::DiagnosticKind::UnknownValueType => "unknown_value_type",
variable_wire::DiagnosticKind::InvalidBooleanEncoding => "invalid_boolean_encoding",
variable_wire::DiagnosticKind::InvalidNumberEncoding => "invalid_number_encoding",
variable_wire::DiagnosticKind::InvalidUtf8String => "invalid_utf8_string",
}
}
fn diagnostic_severity_str(severity: variable_wire::DiagnosticSeverity) -> &'static str {
match severity {
variable_wire::DiagnosticSeverity::Info => "info",
variable_wire::DiagnosticSeverity::Warning => "warning",
variable_wire::DiagnosticSeverity::Error => "error",
}
}