use std::collections::HashMap;
use std::error::Error as StdError;
use serde::{Deserialize, Serialize};
use crate::error::{Diagnostic, Severity};
use crate::value::QuillValue;
use super::{CardSchema, FieldSchema, UiContainerSchema, UiFieldSchema};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct QuillConfig {
pub name: String,
pub cards: Vec<CardSchema>,
pub backend: String,
pub version: String,
pub author: String,
pub example_file: Option<String>,
pub plate_file: Option<String>,
#[serde(flatten)]
pub metadata: HashMap<String, QuillValue>,
#[serde(default)]
pub typst_config: HashMap<String, QuillValue>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct CardSchemaDef {
pub title: Option<String>,
pub description: Option<String>,
pub fields: Option<serde_json::Map<String, serde_json::Value>>,
pub ui: Option<UiContainerSchema>,
}
impl QuillConfig {
pub fn main(&self) -> &CardSchema {
&self.cards[0]
}
pub fn card_definitions(&self) -> &[CardSchema] {
&self.cards[1..]
}
pub fn card_definitions_map(&self) -> HashMap<String, CardSchema> {
self.card_definitions()
.iter()
.map(|card| (card.name.clone(), card.clone()))
.collect()
}
pub fn card_definition(&self, name: &str) -> Option<&CardSchema> {
self.card_definitions()
.iter()
.find(|card| card.name == name)
}
pub fn extract_defaults(&self) -> HashMap<String, QuillValue> {
let mut defaults = HashMap::new();
for (field_name, field_schema) in &self.main().fields {
if let Some(ref default_value) = field_schema.default {
defaults.insert(field_name.clone(), default_value.clone());
}
}
defaults
}
pub fn extract_examples(&self) -> HashMap<String, Vec<QuillValue>> {
let mut examples = HashMap::new();
for (field_name, field_schema) in &self.main().fields {
if let Some(ref examples_value) = field_schema.examples {
if let Some(examples_array) = examples_value.as_array() {
let examples_vec: Vec<QuillValue> = examples_array
.iter()
.map(|v| QuillValue::from_json(v.clone()))
.collect();
if !examples_vec.is_empty() {
examples.insert(field_name.clone(), examples_vec);
}
}
}
}
examples
}
pub fn coerce_fields(
&self,
fields: &HashMap<String, QuillValue>,
) -> HashMap<String, QuillValue> {
let mut coerced = HashMap::new();
for (field_name, field_value) in fields {
if let Some(field_schema) = self.main().fields.get(field_name) {
coerced.insert(
field_name.clone(),
Self::coerce_value(field_value, &field_schema.r#type),
);
} else {
coerced.insert(field_name.clone(), field_value.clone());
}
}
if let Some(cards_value) = coerced.get("CARDS") {
if let Some(cards_array) = cards_value.as_array() {
let coerced_cards = self.coerce_cards_array(cards_array);
coerced.insert(
"CARDS".to_string(),
QuillValue::from_json(serde_json::Value::Array(coerced_cards)),
);
}
}
coerced
}
fn coerce_cards_array(&self, cards_array: &[serde_json::Value]) -> Vec<serde_json::Value> {
let mut coerced_cards = Vec::new();
for card in cards_array {
if let Some(card_obj) = card.as_object() {
if let Some(card_type) = card_obj.get("CARD").and_then(|v| v.as_str()) {
if let Some(card_schema) = self.card_definition(card_type) {
let mut coerced_card = serde_json::Map::new();
for (k, v) in card_obj {
if let Some(field_schema) = card_schema.fields.get(k) {
let qv = QuillValue::from_json(v.clone());
coerced_card.insert(
k.clone(),
Self::coerce_value(&qv, &field_schema.r#type).into_json(),
);
} else {
coerced_card.insert(k.clone(), v.clone());
}
}
coerced_cards.push(serde_json::Value::Object(coerced_card));
continue;
}
}
}
coerced_cards.push(card.clone());
}
coerced_cards
}
fn coerce_value(value: &QuillValue, expected_type: &super::FieldType) -> QuillValue {
use super::FieldType;
let json_value = value.as_json();
match expected_type {
FieldType::Array => {
if json_value.is_array() {
return value.clone();
}
QuillValue::from_json(serde_json::Value::Array(vec![json_value.clone()]))
}
FieldType::Boolean => {
if let Some(b) = json_value.as_bool() {
return QuillValue::from_json(serde_json::Value::Bool(b));
}
if let Some(s) = json_value.as_str() {
let lower = s.to_lowercase();
if lower == "true" {
return QuillValue::from_json(serde_json::Value::Bool(true));
} else if lower == "false" {
return QuillValue::from_json(serde_json::Value::Bool(false));
}
}
if let Some(n) = json_value.as_i64() {
return QuillValue::from_json(serde_json::Value::Bool(n != 0));
}
if let Some(n) = json_value.as_f64() {
if n.is_nan() {
return QuillValue::from_json(serde_json::Value::Bool(false));
}
return QuillValue::from_json(serde_json::Value::Bool(n.abs() > f64::EPSILON));
}
value.clone()
}
FieldType::Number => {
if json_value.is_number() {
return value.clone();
}
if let Some(s) = json_value.as_str() {
if let Ok(i) = s.parse::<i64>() {
return QuillValue::from_json(serde_json::Number::from(i).into());
}
if let Ok(f) = s.parse::<f64>() {
if let Some(num) = serde_json::Number::from_f64(f) {
return QuillValue::from_json(num.into());
}
}
}
if let Some(b) = json_value.as_bool() {
let n = if b { 1 } else { 0 };
return QuillValue::from_json(serde_json::Value::Number(
serde_json::Number::from(n),
));
}
value.clone()
}
FieldType::String | FieldType::Date | FieldType::DateTime | FieldType::Markdown => {
if json_value.is_string() {
return value.clone();
}
if let Some(arr) = json_value.as_array() {
if arr.len() == 1 {
if let Some(s) = arr[0].as_str() {
return QuillValue::from_json(serde_json::Value::String(s.to_string()));
}
}
}
value.clone()
}
FieldType::Object => value.clone(),
}
}
fn parse_fields_with_order(
fields_map: &serde_json::Map<String, serde_json::Value>,
key_order: &[String],
context: &str,
warnings: &mut Vec<Diagnostic>,
) -> HashMap<String, FieldSchema> {
let mut fields = HashMap::new();
let mut fallback_counter = 0;
for (field_name, field_value) in fields_map {
let order = if let Some(idx) = key_order.iter().position(|k| k == field_name) {
idx as i32
} else {
let o = key_order.len() as i32 + fallback_counter;
fallback_counter += 1;
o
};
let quill_value = QuillValue::from_json(field_value.clone());
match FieldSchema::from_quill_value(field_name.clone(), &quill_value) {
Ok(mut schema) => {
if schema.ui.is_none() {
schema.ui = Some(UiFieldSchema {
group: None,
order: Some(order),
visible_when: None,
compact: None,
});
} else if let Some(ui) = &mut schema.ui {
if ui.order.is_none() {
ui.order = Some(order);
}
}
fields.insert(field_name.clone(), schema);
}
Err(e) => {
warnings.push(
Diagnostic::new(
Severity::Warning,
format!("Failed to parse {} '{}': {}", context, field_name, e),
)
.with_code("quill::field_parse_warning".to_string()),
);
}
}
}
fields
}
pub fn from_yaml(yaml_content: &str) -> Result<Self, Box<dyn StdError + Send + Sync>> {
let (config, _warnings) = Self::from_yaml_with_warnings(yaml_content)?;
Ok(config)
}
pub fn from_yaml_with_warnings(
yaml_content: &str,
) -> Result<(Self, Vec<Diagnostic>), Box<dyn StdError + Send + Sync>> {
let mut warnings = Vec::new();
let quill_yaml_val: serde_json::Value = serde_saphyr::from_str(yaml_content)
.map_err(|e| format!("Failed to parse Quill.yaml: {}", e))?;
let quill_section = quill_yaml_val
.get("Quill")
.ok_or("Missing required 'Quill' section in Quill.yaml")?;
let name = quill_section
.get("name")
.and_then(|v| v.as_str())
.ok_or("Missing required 'name' field in 'Quill' section")?
.to_string();
let backend = quill_section
.get("backend")
.and_then(|v| v.as_str())
.ok_or("Missing required 'backend' field in 'Quill' section")?
.to_string();
let description = quill_section
.get("description")
.and_then(|v| v.as_str())
.ok_or("Missing required 'description' field in 'Quill' section")?;
if description.trim().is_empty() {
return Err("'description' field in 'Quill' section cannot be empty".into());
}
let description = description.to_string();
let version_val = quill_section
.get("version")
.ok_or("Missing required 'version' field in 'Quill' section")?;
let version = if let Some(s) = version_val.as_str() {
s.to_string()
} else if let Some(n) = version_val.as_f64() {
n.to_string()
} else {
return Err("Invalid 'version' field format".into());
};
use std::str::FromStr;
crate::version::Version::from_str(&version)
.map_err(|e| format!("Invalid version '{}': {}", version, e))?;
let author = quill_section
.get("author")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| "Unknown".to_string());
let example_file = quill_section
.get("example_file")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let plate_file = quill_section
.get("plate_file")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let ui_section: Option<UiContainerSchema> = quill_section
.get("ui")
.cloned()
.and_then(|v| serde_json::from_value(v).ok());
let mut metadata = HashMap::new();
if let Some(table) = quill_section.as_object() {
for (key, value) in table {
if key != "name"
&& key != "backend"
&& key != "description"
&& key != "version"
&& key != "author"
&& key != "example_file"
&& key != "plate_file"
&& key != "ui"
{
metadata.insert(key.clone(), QuillValue::from_json(value.clone()));
}
}
}
let mut typst_config = HashMap::new();
if let Some(typst_val) = quill_yaml_val.get("typst") {
if let Some(table) = typst_val.as_object() {
for (key, value) in table {
typst_config.insert(key.clone(), QuillValue::from_json(value.clone()));
}
}
}
let main_obj_opt = quill_yaml_val.get("main").and_then(|v| v.as_object());
let fields = if let Some(main_obj) = main_obj_opt {
if let Some(fields_val) = main_obj.get("fields") {
if let Some(fields_map) = fields_val.as_object() {
let field_order: Vec<String> = fields_map.keys().cloned().collect();
Self::parse_fields_with_order(
fields_map,
&field_order,
"field schema",
&mut warnings,
)
} else {
HashMap::new()
}
} else {
HashMap::new()
}
} else if let Some(fields_val) = quill_yaml_val.get("fields") {
if let Some(fields_map) = fields_val.as_object() {
let field_order: Vec<String> = fields_map.keys().cloned().collect();
Self::parse_fields_with_order(
fields_map,
&field_order,
"field schema",
&mut warnings,
)
} else {
HashMap::new()
}
} else {
HashMap::new()
};
let main_ui: Option<UiContainerSchema> = main_obj_opt
.and_then(|main_obj| main_obj.get("ui"))
.cloned()
.and_then(|v| serde_json::from_value(v).ok());
let mut cards: Vec<CardSchema> = vec![CardSchema {
name: "main".to_string(),
title: Some("main".to_string()),
description: Some(description),
fields,
ui: main_ui.or(ui_section),
}];
if let Some(cards_val) = quill_yaml_val.get("cards") {
let cards_table = cards_val
.as_object()
.ok_or("'cards' section must be an object")?;
for (card_name, card_value) in cards_table {
let card_def: CardSchemaDef = serde_json::from_value(card_value.clone())
.map_err(|e| format!("Failed to parse card '{}': {}", card_name, e))?;
let card_fields = if let Some(card_fields_table) =
card_value.get("fields").and_then(|v| v.as_object())
{
let card_field_order: Vec<String> = card_fields_table.keys().cloned().collect();
Self::parse_fields_with_order(
card_fields_table,
&card_field_order,
&format!("card '{}' field", card_name),
&mut warnings,
)
} else if let Some(_toml_fields) = &card_def.fields {
HashMap::new()
} else {
HashMap::new()
};
let card_schema = CardSchema {
name: card_name.clone(),
title: card_def.title,
description: card_def.description,
fields: card_fields,
ui: card_def.ui,
};
cards.push(card_schema);
}
}
Ok((
QuillConfig {
name,
cards,
backend,
version,
author,
example_file,
plate_file,
metadata,
typst_config,
},
warnings,
))
}
}