use dbmd_core::parser::{FieldSpec, Schema, Shape};
use crate::cli::SchemaArgs;
use crate::cmd::write::open_store;
use crate::context::Context;
use crate::error::CliResult;
use crate::sanitize::sanitize_single_line;
pub fn run(ctx: &Context, args: &SchemaArgs) -> CliResult {
let store = open_store(&args.dir)?;
let selected: Vec<(&String, &Schema)> = store
.config
.schemas
.iter()
.filter(|(name, _)| match &args.r#type {
Some(t) => *name == t,
None => true,
})
.collect();
if ctx.json {
print!("{}", schemas_json(&selected));
} else {
print!("{}", schemas_text(&selected));
}
Ok(())
}
fn schemas_text(schemas: &[(&String, &Schema)]) -> String {
let mut out = String::new();
for (i, (name, schema)) in schemas.iter().enumerate() {
if i > 0 {
out.push('\n');
}
out.push_str(&format!("### {}\n", sanitize_single_line(name)));
for field in &schema.fields {
out.push_str(&sanitize_single_line(&field_bullet(field)));
out.push('\n');
}
for key in &schema.unique_keys {
out.push_str(&sanitize_single_line(&format!(
"- unique: {}",
key.join(", ")
)));
out.push('\n');
}
if let Some(template) = &schema.summary_template {
out.push_str(&sanitize_single_line(&format!(
"- summary_template: {template}"
)));
out.push('\n');
}
if let Some(shard) = schema.shard {
out.push_str(&format!("- shard: {}\n", shard_word(shard)));
}
}
out
}
fn schemas_json(schemas: &[(&String, &Schema)]) -> String {
let mut types = serde_json::Map::new();
for (name, schema) in schemas {
types.insert(name.to_string(), schema_json(schema));
}
let obj = serde_json::json!({ "types": types });
let mut s = serde_json::to_string_pretty(&obj).unwrap_or_else(|_| "{}".to_string());
s.push('\n');
s
}
fn schema_json(schema: &Schema) -> serde_json::Value {
serde_json::json!({
"fields": schema.fields.iter().map(field_json).collect::<Vec<_>>(),
"unique": schema.unique_keys,
"summary_template": schema.summary_template,
"shard": schema.shard.map(shard_word),
})
}
fn field_json(field: &FieldSpec) -> serde_json::Value {
serde_json::json!({
"name": field.name,
"required": field.required,
"shape": field.shape.map(shape_word),
"link_prefix": field
.link_prefix
.as_ref()
.map(|p| p.to_string_lossy().replace('\\', "/")),
"default": field
.default
.as_ref()
.map(|v| serde_json::to_value(v).unwrap_or(serde_json::Value::Null)),
"enum": field.enum_values,
"unknown_modifiers": field.unknown_modifiers,
})
}
fn field_bullet(field: &FieldSpec) -> String {
let mut mods: Vec<String> = Vec::new();
if field.required {
mods.push("required".to_string());
}
if let Some(shape) = field.shape {
mods.push(shape_word(shape).to_string());
}
if let Some(prefix) = &field.link_prefix {
mods.push(format!(
"link to {}/",
prefix.to_string_lossy().replace('\\', "/")
));
}
if let Some(default) = &field.default {
mods.push(format!("default {}", scalar_text(default)));
}
if let Some(values) = &field.enum_values {
mods.push(format!("enum: {}", values.join(", ")));
}
mods.extend(field.unknown_modifiers.iter().cloned());
if mods.is_empty() {
format!("- {}", field.name)
} else {
format!("- {} ({})", field.name, mods.join(", "))
}
}
fn scalar_text(value: &serde_norway::Value) -> String {
match value {
serde_norway::Value::String(s) => s.clone(),
other => serde_norway::to_string(other)
.map(|s| s.trim_end().to_string())
.unwrap_or_default(),
}
}
fn shape_word(shape: Shape) -> &'static str {
match shape {
Shape::String => "string",
Shape::Int => "int",
Shape::Bool => "bool",
Shape::Date => "date",
Shape::Email => "email",
Shape::Currency => "currency",
Shape::Url => "url",
}
}
fn shard_word(by_date: bool) -> &'static str {
if by_date {
"by-date"
} else {
"flat"
}
}