Add source files: main, lib, cli, error, convert, highlight, validate, typescript
This commit is contained in:
391
src/convert.rs
Normal file
391
src/convert.rs
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
use serde_json::Value;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::error::{ConfigForgeError, Result};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ConfigFormat {
|
||||||
|
Json,
|
||||||
|
Yaml,
|
||||||
|
Toml,
|
||||||
|
Env,
|
||||||
|
Ini,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ConfigFormat {
|
||||||
|
type Err = ConfigForgeError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"json" => Ok(ConfigFormat::Json),
|
||||||
|
"yaml" | "yml" => Ok(ConfigFormat::Yaml),
|
||||||
|
"toml" => Ok(ConfigFormat::Toml),
|
||||||
|
"env" | "dotenv" => Ok(ConfigFormat::Env),
|
||||||
|
"ini" => Ok(ConfigFormat::Ini),
|
||||||
|
_ => Err(ConfigForgeError::UnsupportedFormat {
|
||||||
|
format: s.to_string(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ConfigFormat {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ConfigFormat::Json => write!(f, "json"),
|
||||||
|
ConfigFormat::Yaml => write!(f, "yaml"),
|
||||||
|
ConfigFormat::Toml => write!(f, "toml"),
|
||||||
|
ConfigFormat::Env => write!(f, "env"),
|
||||||
|
ConfigFormat::Ini => write!(f, "ini"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detect_format(path: &str) -> ConfigFormat {
|
||||||
|
let ext = path.split('.').last().unwrap_or("").to_lowercase();
|
||||||
|
match ext.as_str() {
|
||||||
|
"json" => ConfigFormat::Json,
|
||||||
|
"yaml" | "yml" => ConfigFormat::Yaml,
|
||||||
|
"toml" => ConfigFormat::Toml,
|
||||||
|
"env" | "dotenv" => ConfigFormat::Env,
|
||||||
|
"ini" | "cfg" | "conf" => ConfigFormat::Ini,
|
||||||
|
_ => ConfigFormat::Json,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detect_format_from_content(content: &str) -> Result<ConfigFormat> {
|
||||||
|
let trimmed = content.trim_start();
|
||||||
|
if trimmed.starts_with('{') || trimmed.starts_with('[') {
|
||||||
|
return Ok(ConfigFormat::Json);
|
||||||
|
}
|
||||||
|
if trimmed.starts_with('#') || trimmed.starts_with("---") || trimmed.contains(": ") && !trimmed.contains("= ") {
|
||||||
|
return Ok(ConfigFormat::Yaml);
|
||||||
|
}
|
||||||
|
if trimmed.contains('=') && !trimmed.contains(" = ") && !trimmed.starts_with('[') {
|
||||||
|
return Ok(ConfigFormat::Env);
|
||||||
|
}
|
||||||
|
if trimmed.starts_with('[') {
|
||||||
|
return Ok(ConfigFormat::Ini);
|
||||||
|
}
|
||||||
|
if trimmed.contains(" = ") {
|
||||||
|
return Ok(ConfigFormat::Toml);
|
||||||
|
}
|
||||||
|
Ok(ConfigFormat::Json)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_content(content: &str, format: ConfigFormat) -> Result<Value> {
|
||||||
|
match format {
|
||||||
|
ConfigFormat::Json => parse_json(content),
|
||||||
|
ConfigFormat::Yaml => parse_yaml(content),
|
||||||
|
ConfigFormat::Toml => parse_toml(content),
|
||||||
|
ConfigFormat::Env => parse_env(content),
|
||||||
|
ConfigFormat::Ini => parse_ini(content),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_json(content: &str) -> Result<Value> {
|
||||||
|
serde_json::from_str(content).map_err(|e| ConfigForgeError::ParseError {
|
||||||
|
format: "JSON".to_string(),
|
||||||
|
details: e.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_yaml(content: &str) -> Result<Value> {
|
||||||
|
serde_yaml::from_str(content).map_err(|e| ConfigForgeError::ParseError {
|
||||||
|
format: "YAML".to_string(),
|
||||||
|
details: e.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_toml(content: &str) -> Result<Value> {
|
||||||
|
let value: toml::Value = toml::from_str(content).map_err(|e| ConfigForgeError::ParseError {
|
||||||
|
format: "TOML".to_string(),
|
||||||
|
details: e.to_string(),
|
||||||
|
})?;
|
||||||
|
Ok(toml_to_value(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toml_to_value(value: toml::Value) -> Value {
|
||||||
|
match value {
|
||||||
|
toml::Value::String(s) => Value::String(s),
|
||||||
|
toml::Value::Integer(i) => Value::Number(serde_json::Number::from(i)),
|
||||||
|
toml::Value::Float(f) => {
|
||||||
|
if let Some(n) = serde_json::Number::from_f64(f) {
|
||||||
|
Value::Number(n)
|
||||||
|
} else {
|
||||||
|
Value::Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toml::Value::Boolean(b) => Value::Bool(b),
|
||||||
|
toml::Value::Datetime(dt) => Value::String(dt.to_string()),
|
||||||
|
toml::Value::Array(arr) => Value::Array(arr.into_iter().map(toml_to_value).collect()),
|
||||||
|
toml::Value::Table(table) => {
|
||||||
|
let mut map = serde_json::Map::new();
|
||||||
|
for (k, v) in table {
|
||||||
|
map.insert(k, toml_to_value(v));
|
||||||
|
}
|
||||||
|
Value::Object(map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_env(content: &str) -> Result<Value> {
|
||||||
|
let mut map = serde_json::Map::new();
|
||||||
|
for line in content.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.is_empty() || line.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(eq_idx) = line.find('=') {
|
||||||
|
let key = line[..eq_idx].trim().to_string();
|
||||||
|
let value = line[eq_idx + 1..].trim().to_string();
|
||||||
|
let value = value.trim_matches('"').trim_matches('\'').to_string();
|
||||||
|
map.insert(key, Value::String(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Value::Object(map))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_ini(content: &str) -> Result<Value> {
|
||||||
|
let ini = ini::Ini::load_from_str(content).map_err(|e| ConfigForgeError::ParseError {
|
||||||
|
format: "INI".to_string(),
|
||||||
|
details: e.to_string(),
|
||||||
|
})?;
|
||||||
|
let mut map = serde_json::Map::new();
|
||||||
|
for (section, prop) in ini.iter() {
|
||||||
|
let mut section_map = serde_json::Map::new();
|
||||||
|
if let Some(section_name) = section {
|
||||||
|
for (k, v) in prop.iter() {
|
||||||
|
section_map.insert(k.clone(), Value::String(v.clone().unwrap_or_default()));
|
||||||
|
}
|
||||||
|
map.insert(section_name.clone(), Value::Object(section_map));
|
||||||
|
} else {
|
||||||
|
for (k, v) in prop.iter() {
|
||||||
|
map.insert(k.clone(), Value::String(v.clone().unwrap_or_default()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Value::Object(map))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn convert(value: Value, to_format: ConfigFormat) -> Result<String> {
|
||||||
|
match to_format {
|
||||||
|
ConfigFormat::Json => to_json(&value),
|
||||||
|
ConfigFormat::Yaml => to_yaml(&value),
|
||||||
|
ConfigFormat::Toml => to_toml(&value),
|
||||||
|
ConfigFormat::Env => to_env(&value),
|
||||||
|
ConfigFormat::Ini => to_ini(&value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_json(value: &Value) -> Result<String> {
|
||||||
|
serde_json::to_string_pretty(value).map_err(|e| ConfigForgeError::ConversionError {
|
||||||
|
details: e.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_yaml(value: &Value) -> Result<String> {
|
||||||
|
serde_yaml::to_string(value).map_err(|e| ConfigForgeError::ConversionError {
|
||||||
|
details: e.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_toml(value: &Value) -> Result<String> {
|
||||||
|
let toml_value = value_to_toml(value)?;
|
||||||
|
toml::to_string_pretty(&toml_value).map_err(|e| ConfigForgeError::ConversionError {
|
||||||
|
details: e.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value_to_toml(value: &Value) -> Result<toml::Value> {
|
||||||
|
match value {
|
||||||
|
Value::String(s) => Ok(toml::Value::String(s.clone())),
|
||||||
|
Value::Number(n) => {
|
||||||
|
if let Some(i) = n.as_i64() {
|
||||||
|
Ok(toml::Value::Integer(i))
|
||||||
|
} else if let Some(f) = n.as_f64() {
|
||||||
|
Ok(toml::Value::Float(f))
|
||||||
|
} else {
|
||||||
|
Ok(toml::Value::String(n.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Bool(b) => Ok(toml::Value::Boolean(*b)),
|
||||||
|
Value::Null => Ok(toml::Value::String(String::new())),
|
||||||
|
Value::Array(arr) => {
|
||||||
|
let converted: Result<Vec<toml::Value>> = arr.iter().map(value_to_toml).collect();
|
||||||
|
Ok(toml::Value::Array(converted?))
|
||||||
|
}
|
||||||
|
Value::Object(map) => {
|
||||||
|
let mut table = toml::Map::new();
|
||||||
|
for (k, v) in map {
|
||||||
|
table.insert(k.clone(), value_to_toml(v)?);
|
||||||
|
}
|
||||||
|
Ok(toml::Value::Table(table))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_env(value: &Value) -> Result<String> {
|
||||||
|
match value {
|
||||||
|
Value::Object(map) => {
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
for (k, v) in map {
|
||||||
|
let env_value = match v {
|
||||||
|
Value::String(s) => s.clone(),
|
||||||
|
Value::Number(n) => n.to_string(),
|
||||||
|
Value::Bool(b) => b.to_string(),
|
||||||
|
Value::Null => String::new(),
|
||||||
|
_ => serde_json::to_string(v).unwrap_or_default(),
|
||||||
|
};
|
||||||
|
lines.push(format!("{}={}", k.to_uppercase(), env_value));
|
||||||
|
}
|
||||||
|
Ok(lines.join("\n"))
|
||||||
|
}
|
||||||
|
_ => Err(ConfigForgeError::ConversionError {
|
||||||
|
details: "ENV format requires an object at root".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_ini(value: &Value) -> Result<String> {
|
||||||
|
let mut output = String::new();
|
||||||
|
match value {
|
||||||
|
Value::Object(map) => {
|
||||||
|
let mut has_global = false;
|
||||||
|
for (k, v) in map {
|
||||||
|
if let Value::Object(section) = v {
|
||||||
|
output.push('[');
|
||||||
|
output.push_str(k);
|
||||||
|
output.push_str("]\n");
|
||||||
|
for (key, val) in section {
|
||||||
|
output.push_str(key);
|
||||||
|
output.push_str(" = ");
|
||||||
|
if let Value::String(s) = val {
|
||||||
|
output.push_str(s);
|
||||||
|
} else {
|
||||||
|
output.push_str(&val.to_string());
|
||||||
|
}
|
||||||
|
output.push('\n');
|
||||||
|
}
|
||||||
|
output.push('\n');
|
||||||
|
} else {
|
||||||
|
has_global = true;
|
||||||
|
output.push_str(k);
|
||||||
|
output.push_str(" = ");
|
||||||
|
if let Value::String(s) = v {
|
||||||
|
output.push_str(s);
|
||||||
|
} else {
|
||||||
|
output.push_str(&v.to_string());
|
||||||
|
}
|
||||||
|
output.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if has_global {
|
||||||
|
output.push('\n');
|
||||||
|
}
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
_ => Err(ConfigForgeError::ConversionError {
|
||||||
|
details: "INI format requires an object at root".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_file(path: &std::path::Path) -> Result<String> {
|
||||||
|
std::fs::read_to_string(path).map_err(|_| ConfigForgeError::FileNotFound {
|
||||||
|
path: path.to_string_lossy().to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_file(path: &std::path::Path, content: &str) -> Result<()> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
std::fs::write(path, content)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn infer_schema(value: &Value) -> Value {
|
||||||
|
match value {
|
||||||
|
Value::Null => json!({"type": "null"}),
|
||||||
|
Value::Bool(_) => json!({"type": "boolean"}),
|
||||||
|
Value::Number(n) => {
|
||||||
|
if n.is_i64() {
|
||||||
|
json!({"type": "integer"})
|
||||||
|
} else if n.is_f64() {
|
||||||
|
json!({"type": "number"})
|
||||||
|
} else {
|
||||||
|
json!({"type": "number"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::String(s) => {
|
||||||
|
if s.starts_with('/') && s.ends_with('/') {
|
||||||
|
json!({"type": "string", "format": "regex"})
|
||||||
|
} else if s.contains('@') {
|
||||||
|
json!({"type": "string", "format": "email"})
|
||||||
|
} else {
|
||||||
|
json!({"type": "string"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Array(arr) => {
|
||||||
|
if arr.is_empty() {
|
||||||
|
json!({"type": "array", "items": {}})
|
||||||
|
} else {
|
||||||
|
let mut types = std::collections::HashSet::new();
|
||||||
|
let mut items = serde_json::Map::new();
|
||||||
|
for item in arr {
|
||||||
|
let item_schema = infer_schema(item);
|
||||||
|
if let Some(type_) = item_schema.get("type") {
|
||||||
|
types.insert(type_.clone());
|
||||||
|
}
|
||||||
|
for (k, v) in item_schema.as_object().unwrap_or(&serde_json::Map::new()) {
|
||||||
|
if let Some(existing) = items.get(k) {
|
||||||
|
if existing != v {
|
||||||
|
items.remove(k);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items.insert(k.clone(), v.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut result = serde_json::Map::new();
|
||||||
|
result.insert("type".to_string(), json!("array"));
|
||||||
|
if !types.is_empty() && types.len() == 1 {
|
||||||
|
result.insert("items".to_string(), json!({"type": types.iter().next().unwrap()}));
|
||||||
|
} else {
|
||||||
|
let mut any_of = Vec::new();
|
||||||
|
for type_ in types {
|
||||||
|
any_of.push(json!({"type": type_}));
|
||||||
|
}
|
||||||
|
if !any_of.is_empty() {
|
||||||
|
result.insert("items".to_string(), json!({"anyOf": any_of}));
|
||||||
|
} else {
|
||||||
|
result.insert("items".to_string(), json!({}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Object(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Object(map) => {
|
||||||
|
let mut properties = serde_json::Map::new();
|
||||||
|
let mut required = Vec::new();
|
||||||
|
|
||||||
|
for (k, v) in map {
|
||||||
|
properties.insert(k.clone(), infer_schema(v));
|
||||||
|
if k.starts_with('_') || k.ends_with('?') || k.starts_with("optional_") {
|
||||||
|
} else {
|
||||||
|
required.push(k.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result = serde_json::Map::new();
|
||||||
|
result.insert("type".to_string(), json!("object"));
|
||||||
|
result.insert("properties".to_string(), Value::Object(properties));
|
||||||
|
if !required.is_empty() {
|
||||||
|
result.insert("required".to_string(), json!(required));
|
||||||
|
}
|
||||||
|
Value::Object(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user