use crate::quill::{field_key, ui_key, CardSchema, FieldSchema, FieldType};
use crate::{QuillValue, RenderError};
use serde_json::{json, Map, Value};
use std::collections::HashMap;
fn build_field_property(field_schema: &FieldSchema) -> Map<String, Value> {
let mut property = Map::new();
let (json_type, format, content_media_type) = match field_schema.r#type {
FieldType::String => ("string", None, None),
FieldType::Number => ("number", None, None),
FieldType::Boolean => ("boolean", None, None),
FieldType::Array => ("array", None, None),
FieldType::Object => ("object", None, None),
FieldType::Date => ("string", Some("date"), None),
FieldType::DateTime => ("string", Some("date-time"), None),
FieldType::Markdown => ("string", None, Some("text/markdown")),
};
property.insert(
field_key::TYPE.to_string(),
Value::String(json_type.to_string()),
);
if let Some(fmt) = format {
property.insert(
field_key::FORMAT.to_string(),
Value::String(fmt.to_string()),
);
}
if let Some(media_type) = content_media_type {
property.insert(
"contentMediaType".to_string(),
Value::String(media_type.to_string()),
);
}
if let Some(ref title) = field_schema.title {
property.insert(field_key::TITLE.to_string(), Value::String(title.clone()));
}
if let Some(ref description) = field_schema.description {
property.insert(
field_key::DESCRIPTION.to_string(),
Value::String(description.clone()),
);
}
if let Some(ref ui) = field_schema.ui {
let mut ui_obj = Map::new();
if let Some(ref group) = ui.group {
ui_obj.insert(ui_key::GROUP.to_string(), json!(group));
}
if let Some(order) = ui.order {
ui_obj.insert(ui_key::ORDER.to_string(), json!(order));
}
if let Some(ref visible_when) = ui.visible_when {
let mut vw_obj = Map::new();
for (field_name, values) in visible_when {
let values_array: Vec<Value> =
values.iter().map(|v| Value::String(v.clone())).collect();
vw_obj.insert(field_name.clone(), Value::Array(values_array));
}
ui_obj.insert(ui_key::VISIBLE_WHEN.to_string(), Value::Object(vw_obj));
}
if !ui_obj.is_empty() {
property.insert("x-ui".to_string(), Value::Object(ui_obj));
}
}
if let Some(ref examples) = field_schema.examples {
if let Some(examples_array) = examples.as_array() {
if !examples_array.is_empty() {
property.insert(
field_key::EXAMPLES.to_string(),
Value::Array(examples_array.clone()),
);
}
}
}
if let Some(ref default) = field_schema.default {
property.insert(field_key::DEFAULT.to_string(), default.as_json().clone());
}
if let Some(ref enum_values) = field_schema.enum_values {
let enum_array: Vec<Value> = enum_values
.iter()
.map(|s| Value::String(s.clone()))
.collect();
property.insert(field_key::ENUM.to_string(), Value::Array(enum_array));
}
if let Some(ref properties) = field_schema.properties {
let mut props_map = Map::new();
let mut required_fields = Vec::new();
for (prop_name, prop_schema) in properties {
props_map.insert(
prop_name.clone(),
Value::Object(build_field_property(prop_schema)),
);
if prop_schema.required {
required_fields.push(Value::String(prop_name.clone()));
}
}
property.insert("properties".to_string(), Value::Object(props_map));
if !required_fields.is_empty() {
property.insert("required".to_string(), Value::Array(required_fields));
}
}
if let Some(ref items) = field_schema.items {
property.insert(
"items".to_string(),
Value::Object(build_field_property(items)),
);
}
property
}
fn build_card_def(name: &str, card: &CardSchema) -> Map<String, Value> {
let mut def = Map::new();
def.insert("type".to_string(), Value::String("object".to_string()));
if let Some(ref title) = card.title {
def.insert("title".to_string(), Value::String(title.clone()));
}
if let Some(ref description) = card.description {
if !description.is_empty() {
def.insert(
"description".to_string(),
Value::String(description.clone()),
);
}
}
if let Some(ref ui) = card.ui {
let mut ui_obj = Map::new();
if let Some(hide_body) = ui.hide_body {
ui_obj.insert(ui_key::HIDE_BODY.to_string(), Value::Bool(hide_body));
}
if !ui_obj.is_empty() {
def.insert("x-ui".to_string(), Value::Object(ui_obj));
}
}
let mut properties = Map::new();
let mut required = vec![Value::String("CARD".to_string())];
let mut card_prop = Map::new();
card_prop.insert("const".to_string(), Value::String(name.to_string()));
properties.insert("CARD".to_string(), Value::Object(card_prop));
for (field_name, field_schema) in &card.fields {
let field_prop = build_field_property(field_schema);
properties.insert(field_name.clone(), Value::Object(field_prop));
if field_schema.required {
required.push(Value::String(field_name.clone()));
}
}
def.insert("properties".to_string(), Value::Object(properties));
def.insert("required".to_string(), Value::Array(required));
def
}
pub fn build_schema(
document: &CardSchema,
definitions: &HashMap<String, CardSchema>,
) -> Result<QuillValue, RenderError> {
let mut properties = Map::new();
let mut required_fields = Vec::new();
let mut defs = Map::new();
for (field_name, field_schema) in &document.fields {
let property = build_field_property(field_schema);
properties.insert(field_name.clone(), Value::Object(property));
if field_schema.required {
required_fields.push(field_name.clone());
}
}
if !properties.contains_key("BODY") {
let mut body_property = Map::new();
body_property.insert("type".to_string(), Value::String("string".to_string()));
body_property.insert(
"contentMediaType".to_string(),
Value::String("text/markdown".to_string()),
);
properties.insert("BODY".to_string(), Value::Object(body_property));
}
if !definitions.is_empty() {
let mut one_of = Vec::new();
let mut discriminator_mapping = Map::new();
for (card_name, card_schema) in definitions {
let def_name = format!("{}_card", card_name);
let ref_path = format!("#/$defs/{}", def_name);
defs.insert(
def_name.clone(),
Value::Object(build_card_def(card_name, card_schema)),
);
let mut ref_obj = Map::new();
ref_obj.insert("$ref".to_string(), Value::String(ref_path.clone()));
one_of.push(Value::Object(ref_obj));
discriminator_mapping.insert(card_name.clone(), Value::String(ref_path));
}
let mut items_schema = Map::new();
items_schema.insert("oneOf".to_string(), Value::Array(one_of));
let mut cards_property = Map::new();
cards_property.insert("type".to_string(), Value::String("array".to_string()));
cards_property.insert("items".to_string(), Value::Object(items_schema));
properties.insert("CARDS".to_string(), Value::Object(cards_property));
}
let mut schema_map = Map::new();
schema_map.insert(
"$schema".to_string(),
Value::String("https://json-schema.org/draft/2019-09/schema".to_string()),
);
schema_map.insert("type".to_string(), Value::String("object".to_string()));
if !defs.is_empty() {
schema_map.insert("$defs".to_string(), Value::Object(defs));
}
if let Some(ref description) = document.description {
if !description.is_empty() {
schema_map.insert(
"description".to_string(),
Value::String(description.clone()),
);
}
}
if let Some(ref ui) = document.ui {
let mut ui_obj = Map::new();
if let Some(hide_body) = ui.hide_body {
ui_obj.insert(ui_key::HIDE_BODY.to_string(), Value::Bool(hide_body));
}
if !ui_obj.is_empty() {
schema_map.insert("x-ui".to_string(), Value::Object(ui_obj));
}
}
schema_map.insert("properties".to_string(), Value::Object(properties));
schema_map.insert(
"required".to_string(),
Value::Array(required_fields.into_iter().map(Value::String).collect()),
);
let schema = Value::Object(schema_map);
Ok(QuillValue::from_json(schema))
}
pub fn strip_schema_fields(schema: &mut Value, fields: &[&str]) {
match schema {
Value::Object(map) => {
for field in fields {
map.remove(*field);
}
for value in map.values_mut() {
strip_schema_fields(value, fields);
}
}
Value::Array(arr) => {
for item in arr {
strip_schema_fields(item, fields);
}
}
_ => {}
}
}
pub fn build_schema_from_fields(
field_schemas: &HashMap<String, FieldSchema>,
) -> Result<QuillValue, RenderError> {
let document = CardSchema {
name: "root".to_string(),
title: None,
description: None,
fields: field_schemas.clone(),
ui: None,
};
build_schema(&document, &HashMap::new())
}
pub fn extract_defaults_from_schema(
schema: &QuillValue,
) -> HashMap<String, crate::value::QuillValue> {
let mut defaults = HashMap::new();
if let Some(properties) = schema.as_json().get("properties") {
if let Some(properties_obj) = properties.as_object() {
for (field_name, field_schema) in properties_obj {
if let Some(default_value) = field_schema.get("default") {
defaults.insert(
field_name.clone(),
QuillValue::from_json(default_value.clone()),
);
}
}
}
}
defaults
}
pub fn extract_examples_from_schema(
schema: &QuillValue,
) -> HashMap<String, Vec<crate::value::QuillValue>> {
let mut examples = HashMap::new();
if let Some(properties) = schema.as_json().get("properties") {
if let Some(properties_obj) = properties.as_object() {
for (field_name, field_schema) in properties_obj {
if let Some(examples_value) = field_schema.get("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 extract_card_item_defaults(
schema: &QuillValue,
) -> HashMap<String, HashMap<String, QuillValue>> {
let mut card_defaults = HashMap::new();
if let Some(properties) = schema.as_json().get("properties") {
if let Some(properties_obj) = properties.as_object() {
for (field_name, field_schema) in properties_obj {
let is_array = field_schema
.get("type")
.and_then(|t| t.as_str())
.map(|t| t == "array")
.unwrap_or(false);
if !is_array {
continue;
}
if let Some(items_schema) = field_schema.get("items") {
if let Some(item_props) = items_schema.get("properties") {
if let Some(item_props_obj) = item_props.as_object() {
let mut item_defaults = HashMap::new();
for (item_field_name, item_field_schema) in item_props_obj {
if let Some(default_value) = item_field_schema.get("default") {
item_defaults.insert(
item_field_name.clone(),
QuillValue::from_json(default_value.clone()),
);
}
}
if !item_defaults.is_empty() {
card_defaults.insert(field_name.clone(), item_defaults);
}
}
}
}
}
}
}
card_defaults
}
pub fn apply_card_item_defaults(
fields: &HashMap<String, QuillValue>,
card_defaults: &HashMap<String, HashMap<String, QuillValue>>,
) -> HashMap<String, QuillValue> {
let mut result = fields.clone();
for (card_name, item_defaults) in card_defaults {
if let Some(card_value) = result.get(card_name) {
if let Some(items_array) = card_value.as_array() {
let mut updated_items: Vec<serde_json::Value> = Vec::new();
for item in items_array {
if let Some(item_obj) = item.as_object() {
let mut new_item = item_obj.clone();
for (default_field, default_value) in item_defaults {
if !new_item.contains_key(default_field) {
new_item
.insert(default_field.clone(), default_value.as_json().clone());
}
}
updated_items.push(serde_json::Value::Object(new_item));
} else {
updated_items.push(item.clone());
}
}
result.insert(
card_name.clone(),
QuillValue::from_json(serde_json::Value::Array(updated_items)),
);
}
}
}
result
}
pub fn validate_document(
schema: &QuillValue,
fields: &HashMap<String, crate::value::QuillValue>,
) -> Result<(), Vec<String>> {
let mut doc_json = Map::new();
for (key, value) in fields {
doc_json.insert(key.clone(), value.as_json().clone());
}
let doc_value = Value::Object(doc_json);
let compiled = match jsonschema::Validator::new(schema.as_json()) {
Ok(c) => c,
Err(e) => return Err(vec![format!("Failed to compile schema: {}", e)]),
};
let mut all_errors = Vec::new();
if let Some(cards) = doc_value.get("CARDS").and_then(|v| v.as_array()) {
let card_errors = validate_cards_array(schema, cards);
all_errors.extend(card_errors);
}
let validation_result = compiled.validate(&doc_value);
match validation_result {
Ok(_) => {
if all_errors.is_empty() {
Ok(())
} else {
Err(all_errors)
}
}
Err(error) => {
let path = error.instance_path().to_string();
let path_display = if path.is_empty() {
"document".to_string()
} else {
path.clone()
};
let is_generic_card_error = path.starts_with("/CARDS/")
&& error.to_string().contains("oneOf")
&& !all_errors.is_empty();
if !is_generic_card_error {
if path.starts_with("/CARDS/") && error.to_string().contains("oneOf") {
if let Some(rest) = path.strip_prefix("/CARDS/") {
let is_item_error = !rest.contains('/');
if is_item_error {
if let Ok(idx) = rest.parse::<usize>() {
if let Some(cards) =
doc_value.get("CARDS").and_then(|v| v.as_array())
{
if let Some(item) = cards.get(idx) {
if let Some(card_type) =
item.get("CARD").and_then(|v| v.as_str())
{
let mut valid_types = Vec::new();
if let Some(defs) = schema
.as_json()
.get("$defs")
.and_then(|v| v.as_object())
{
for key in defs.keys() {
if let Some(name) = key.strip_suffix("_card") {
valid_types.push(name.to_string());
}
}
}
if !valid_types.is_empty()
&& !valid_types.contains(&card_type.to_string())
{
valid_types.sort();
let valid_list = valid_types.join(", ");
let message = format!("Validation error at {}: Invalid card type '{}'. Valid types are: [{}]", path_display, card_type, valid_list);
all_errors.push(message);
return Err(all_errors);
}
}
}
}
}
}
}
}
let message = format!("Validation error at {}: {}", path_display, error);
all_errors.push(message);
}
Err(all_errors)
}
}
}
fn validate_cards_array(document_schema: &QuillValue, cards_array: &[Value]) -> Vec<String> {
let mut errors = Vec::new();
let defs = document_schema
.as_json()
.get("$defs")
.and_then(|v| v.as_object());
for (idx, card) in cards_array.iter().enumerate() {
if let Some(card_obj) = card.as_object() {
if let Some(card_type) = card_obj.get("CARD").and_then(|v| v.as_str()) {
let def_name = format!("{}_card", card_type);
if let Some(card_schema_json) = defs.and_then(|d| d.get(&def_name)) {
let mut card_fields = HashMap::new();
for (k, v) in card_obj {
card_fields.insert(k.clone(), QuillValue::from_json(v.clone()));
}
if let Err(card_errors) = validate_document(
&QuillValue::from_json(card_schema_json.clone()),
&card_fields,
) {
for err in card_errors {
let prefix = format!("/CARDS/{}", idx);
let new_msg =
if let Some(rest) = err.strip_prefix("Validation error at ") {
if rest.starts_with("document") {
format!(
"Validation error at {}:{}",
prefix,
rest.strip_prefix("document").unwrap_or(rest)
)
} else {
format!("Validation error at {}{}", prefix, rest)
}
} else {
format!("Validation error at {}: {}", prefix, err)
};
errors.push(new_msg);
}
}
}
}
}
}
errors
}
fn coerce_value(value: &QuillValue, expected_type: &str) -> QuillValue {
let json_value = value.as_json();
match expected_type {
"array" => {
if json_value.is_array() {
return value.clone();
}
QuillValue::from_json(Value::Array(vec![json_value.clone()]))
}
"boolean" => {
if let Some(b) = json_value.as_bool() {
return QuillValue::from_json(Value::Bool(b));
}
if let Some(s) = json_value.as_str() {
let lower = s.to_lowercase();
if lower == "true" {
return QuillValue::from_json(Value::Bool(true));
} else if lower == "false" {
return QuillValue::from_json(Value::Bool(false));
}
}
if let Some(n) = json_value.as_i64() {
return QuillValue::from_json(Value::Bool(n != 0));
}
if let Some(n) = json_value.as_f64() {
if n.is_nan() {
return QuillValue::from_json(Value::Bool(false));
}
return QuillValue::from_json(Value::Bool(n.abs() > f64::EPSILON));
}
value.clone()
}
"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 num_value = if b { 1 } else { 0 };
return QuillValue::from_json(Value::Number(serde_json::Number::from(num_value)));
}
value.clone()
}
"string" => {
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(Value::String(s.to_string()));
}
}
}
value.clone()
}
_ => {
value.clone()
}
}
}
pub fn coerce_document(
schema: &QuillValue,
fields: &HashMap<String, QuillValue>,
) -> HashMap<String, QuillValue> {
let mut coerced_fields = HashMap::new();
let properties = match schema.as_json().get("properties") {
Some(props) => props,
None => {
return fields.clone();
}
};
let properties_obj = match properties.as_object() {
Some(obj) => obj,
None => {
return fields.clone();
}
};
for (field_name, field_value) in fields {
if let Some(field_schema) = properties_obj.get(field_name) {
if let Some(expected_type) = field_schema.get("type").and_then(|t| t.as_str()) {
let coerced_value = coerce_value(field_value, expected_type);
coerced_fields.insert(field_name.clone(), coerced_value);
continue;
}
}
coerced_fields.insert(field_name.clone(), field_value.clone());
}
if let Some(cards_value) = coerced_fields.get("CARDS") {
if let Some(cards_array) = cards_value.as_array() {
let coerced_cards = coerce_cards_array(schema, cards_array);
coerced_fields.insert(
"CARDS".to_string(),
QuillValue::from_json(Value::Array(coerced_cards)),
);
}
}
coerced_fields
}
fn coerce_cards_array(document_schema: &QuillValue, cards_array: &[Value]) -> Vec<Value> {
let mut coerced_cards = Vec::new();
let defs = document_schema
.as_json()
.get("$defs")
.and_then(|v| v.as_object());
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()) {
let def_name = format!("{}_card", card_type);
if let Some(card_schema_json) = defs.and_then(|d| d.get(&def_name)) {
let mut card_fields = HashMap::new();
for (k, v) in card_obj {
card_fields.insert(k.clone(), QuillValue::from_json(v.clone()));
}
let coerced_card_fields = coerce_document(
&QuillValue::from_json(card_schema_json.clone()),
&card_fields,
);
let mut coerced_card_obj = Map::new();
for (k, v) in coerced_card_fields {
coerced_card_obj.insert(k, v.into_json());
}
coerced_cards.push(Value::Object(coerced_card_obj));
continue;
}
}
}
coerced_cards.push(card.clone());
}
coerced_cards
}
#[cfg(test)]
mod tests {
use super::*;
use crate::quill::FieldSchema;
use crate::value::QuillValue;
#[test]
fn test_build_schema_simple() {
let mut fields = HashMap::new();
let schema = FieldSchema::new(
"author".to_string(),
FieldType::String,
Some("The name of the author".to_string()),
);
fields.insert("author".to_string(), schema);
let json_schema = build_schema_from_fields(&fields).unwrap().as_json().clone();
assert_eq!(json_schema["type"], "object");
assert_eq!(json_schema["properties"]["author"]["type"], "string");
assert_eq!(
json_schema["properties"]["author"]["description"],
"The name of the author"
);
}
#[test]
fn test_build_schema_with_default() {
let mut fields = HashMap::new();
let mut schema = FieldSchema::new(
"Field with default".to_string(),
FieldType::String,
Some("A field with a default value".to_string()),
);
schema.default = Some(QuillValue::from_json(json!("default value")));
fields.insert("with_default".to_string(), schema);
build_schema_from_fields(&fields).unwrap();
}
#[test]
fn test_build_schema_date_types() {
let mut fields = HashMap::new();
let date_schema = FieldSchema::new(
"Date field".to_string(),
FieldType::Date,
Some("A field for dates".to_string()),
);
fields.insert("date_field".to_string(), date_schema);
let datetime_schema = FieldSchema::new(
"DateTime field".to_string(),
FieldType::DateTime,
Some("A field for date and time".to_string()),
);
fields.insert("datetime_field".to_string(), datetime_schema);
let json_schema = build_schema_from_fields(&fields).unwrap().as_json().clone();
assert_eq!(json_schema["properties"]["date_field"]["type"], "string");
assert_eq!(json_schema["properties"]["date_field"]["format"], "date");
assert_eq!(
json_schema["properties"]["datetime_field"]["type"],
"string"
);
assert_eq!(
json_schema["properties"]["datetime_field"]["format"],
"date-time"
);
}
#[test]
fn test_validate_document_success() {
let schema = json!({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object",
"properties": {
"title": {"type": "string"},
"count": {"type": "number"}
},
"required": ["title"],
"additionalProperties": true
});
let mut fields = HashMap::new();
fields.insert(
"title".to_string(),
QuillValue::from_json(json!("Test Title")),
);
fields.insert("count".to_string(), QuillValue::from_json(json!(42)));
let result = validate_document(&QuillValue::from_json(schema), &fields);
assert!(result.is_ok());
}
#[test]
fn test_validate_document_missing_required() {
let schema = json!({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object",
"properties": {
"title": {"type": "string"}
},
"required": ["title"],
"additionalProperties": true
});
let fields = HashMap::new();
let result = validate_document(&QuillValue::from_json(schema), &fields);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(!errors.is_empty());
}
#[test]
fn test_validate_document_wrong_type() {
let schema = json!({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object",
"properties": {
"count": {"type": "number"}
},
"additionalProperties": true
});
let mut fields = HashMap::new();
fields.insert(
"count".to_string(),
QuillValue::from_json(json!("not a number")),
);
let result = validate_document(&QuillValue::from_json(schema), &fields);
assert!(result.is_err());
}
#[test]
fn test_validate_document_allows_extra_fields() {
let schema = json!({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object",
"properties": {
"title": {"type": "string"}
},
"required": ["title"],
"additionalProperties": true
});
let mut fields = HashMap::new();
fields.insert("title".to_string(), QuillValue::from_json(json!("Test")));
fields.insert("extra".to_string(), QuillValue::from_json(json!("allowed")));
let result = validate_document(&QuillValue::from_json(schema), &fields);
assert!(result.is_ok());
}
#[test]
fn test_build_schema_with_example() {
let mut fields = HashMap::new();
let mut schema = FieldSchema::new(
"memo_for".to_string(),
FieldType::Array,
Some("List of recipient organization symbols".to_string()),
);
schema.examples = Some(QuillValue::from_json(json!([[
"ORG1/SYMBOL",
"ORG2/SYMBOL"
]])));
fields.insert("memo_for".to_string(), schema);
let json_schema = build_schema_from_fields(&fields).unwrap().as_json().clone();
assert!(json_schema["properties"]["memo_for"]
.as_object()
.unwrap()
.contains_key("examples"));
let example_value = &json_schema["properties"]["memo_for"]["examples"][0];
assert_eq!(example_value, &json!(["ORG1/SYMBOL", "ORG2/SYMBOL"]));
}
#[test]
fn test_build_schema_includes_default_in_properties() {
let mut fields = HashMap::new();
let mut schema = FieldSchema::new(
"ice_cream".to_string(),
FieldType::String,
Some("favorite ice cream flavor".to_string()),
);
schema.default = Some(QuillValue::from_json(json!("taro")));
fields.insert("ice_cream".to_string(), schema);
let json_schema = build_schema_from_fields(&fields).unwrap().as_json().clone();
assert!(json_schema["properties"]["ice_cream"]
.as_object()
.unwrap()
.contains_key("default"));
assert_eq!(json_schema["properties"]["ice_cream"]["default"], "taro");
let required_fields = json_schema["required"].as_array().unwrap();
assert!(!required_fields.contains(&json!("ice_cream")));
}
#[test]
fn test_extract_defaults_from_schema() {
let schema = json!({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Document title"
},
"author": {
"type": "string",
"description": "Document author",
"default": "Anonymous"
},
"status": {
"type": "string",
"description": "Document status",
"default": "draft"
},
"count": {
"type": "number",
"default": 42
}
},
"required": ["title"]
});
let defaults = extract_defaults_from_schema(&QuillValue::from_json(schema));
assert_eq!(defaults.len(), 3);
assert!(!defaults.contains_key("title")); assert!(defaults.contains_key("author"));
assert!(defaults.contains_key("status"));
assert!(defaults.contains_key("count"));
assert_eq!(defaults.get("author").unwrap().as_str(), Some("Anonymous"));
assert_eq!(defaults.get("status").unwrap().as_str(), Some("draft"));
assert_eq!(defaults.get("count").unwrap().as_json().as_i64(), Some(42));
}
#[test]
fn test_extract_defaults_from_schema_empty() {
let schema = json!({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object",
"properties": {
"title": {"type": "string"},
"author": {"type": "string"}
},
"required": ["title"]
});
let defaults = extract_defaults_from_schema(&QuillValue::from_json(schema));
assert_eq!(defaults.len(), 0);
}
#[test]
fn test_extract_defaults_from_schema_no_properties() {
let schema = json!({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object"
});
let defaults = extract_defaults_from_schema(&QuillValue::from_json(schema));
assert_eq!(defaults.len(), 0);
}
#[test]
fn test_extract_examples_from_schema() {
let schema = json!({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Document title"
},
"memo_for": {
"type": "array",
"description": "List of recipients",
"examples": [
["ORG1/SYMBOL", "ORG2/SYMBOL"],
["DEPT/OFFICE"]
]
},
"author": {
"type": "string",
"description": "Document author",
"examples": ["John Doe", "Jane Smith"]
},
"status": {
"type": "string",
"description": "Document status"
}
}
});
let examples = extract_examples_from_schema(&QuillValue::from_json(schema));
assert_eq!(examples.len(), 2);
assert!(!examples.contains_key("title")); assert!(examples.contains_key("memo_for"));
assert!(examples.contains_key("author"));
assert!(!examples.contains_key("status"));
let memo_for_examples = examples.get("memo_for").unwrap();
assert_eq!(memo_for_examples.len(), 2);
assert_eq!(
memo_for_examples[0].as_json(),
&json!(["ORG1/SYMBOL", "ORG2/SYMBOL"])
);
assert_eq!(memo_for_examples[1].as_json(), &json!(["DEPT/OFFICE"]));
let author_examples = examples.get("author").unwrap();
assert_eq!(author_examples.len(), 2);
assert_eq!(author_examples[0].as_str(), Some("John Doe"));
assert_eq!(author_examples[1].as_str(), Some("Jane Smith"));
}
#[test]
fn test_extract_examples_from_schema_empty() {
let schema = json!({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object",
"properties": {
"title": {"type": "string"},
"author": {"type": "string"}
}
});
let examples = extract_examples_from_schema(&QuillValue::from_json(schema));
assert_eq!(examples.len(), 0);
}
#[test]
fn test_extract_examples_from_schema_no_properties() {
let schema = json!({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object"
});
let examples = extract_examples_from_schema(&QuillValue::from_json(schema));
assert_eq!(examples.len(), 0);
}
#[test]
fn test_coerce_singular_to_array() {
let schema = json!({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object",
"properties": {
"tags": {"type": "array"}
}
});
let mut fields = HashMap::new();
fields.insert(
"tags".to_string(),
QuillValue::from_json(json!("single-tag")),
);
let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
let tags = coerced.get("tags").unwrap();
assert!(tags.as_array().is_some());
let tags_array = tags.as_array().unwrap();
assert_eq!(tags_array.len(), 1);
assert_eq!(tags_array[0].as_str().unwrap(), "single-tag");
}
#[test]
fn test_coerce_array_unchanged() {
let schema = json!({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object",
"properties": {
"tags": {"type": "array"}
}
});
let mut fields = HashMap::new();
fields.insert(
"tags".to_string(),
QuillValue::from_json(json!(["tag1", "tag2"])),
);
let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
let tags = coerced.get("tags").unwrap();
let tags_array = tags.as_array().unwrap();
assert_eq!(tags_array.len(), 2);
}
#[test]
fn test_coerce_string_to_boolean() {
let schema = json!({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object",
"properties": {
"active": {"type": "boolean"},
"enabled": {"type": "boolean"}
}
});
let mut fields = HashMap::new();
fields.insert("active".to_string(), QuillValue::from_json(json!("true")));
fields.insert("enabled".to_string(), QuillValue::from_json(json!("FALSE")));
let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
assert!(coerced.get("active").unwrap().as_bool().unwrap());
assert!(!coerced.get("enabled").unwrap().as_bool().unwrap());
}
#[test]
fn test_coerce_number_to_boolean() {
let schema = json!({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object",
"properties": {
"flag1": {"type": "boolean"},
"flag2": {"type": "boolean"},
"flag3": {"type": "boolean"}
}
});
let mut fields = HashMap::new();
fields.insert("flag1".to_string(), QuillValue::from_json(json!(0)));
fields.insert("flag2".to_string(), QuillValue::from_json(json!(1)));
fields.insert("flag3".to_string(), QuillValue::from_json(json!(42)));
let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
assert!(!coerced.get("flag1").unwrap().as_bool().unwrap());
assert!(coerced.get("flag2").unwrap().as_bool().unwrap());
assert!(coerced.get("flag3").unwrap().as_bool().unwrap());
}
#[test]
fn test_coerce_float_to_boolean() {
let schema = json!({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object",
"properties": {
"flag1": {"type": "boolean"},
"flag2": {"type": "boolean"},
"flag3": {"type": "boolean"},
"flag4": {"type": "boolean"}
}
});
let mut fields = HashMap::new();
fields.insert("flag1".to_string(), QuillValue::from_json(json!(0.0)));
fields.insert("flag2".to_string(), QuillValue::from_json(json!(0.5)));
fields.insert("flag3".to_string(), QuillValue::from_json(json!(-1.5)));
fields.insert("flag4".to_string(), QuillValue::from_json(json!(1e-100)));
let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
assert!(!coerced.get("flag1").unwrap().as_bool().unwrap());
assert!(coerced.get("flag2").unwrap().as_bool().unwrap());
assert!(coerced.get("flag3").unwrap().as_bool().unwrap());
assert!(!coerced.get("flag4").unwrap().as_bool().unwrap());
}
#[test]
fn test_coerce_string_to_number() {
let schema = json!({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object",
"properties": {
"count": {"type": "number"},
"price": {"type": "number"}
}
});
let mut fields = HashMap::new();
fields.insert("count".to_string(), QuillValue::from_json(json!("42")));
fields.insert("price".to_string(), QuillValue::from_json(json!("19.99")));
let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
assert_eq!(coerced.get("count").unwrap().as_i64().unwrap(), 42);
assert_eq!(coerced.get("price").unwrap().as_f64().unwrap(), 19.99);
}
#[test]
fn test_coerce_boolean_to_number() {
let schema = json!({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object",
"properties": {
"active": {"type": "number"},
"disabled": {"type": "number"}
}
});
let mut fields = HashMap::new();
fields.insert("active".to_string(), QuillValue::from_json(json!(true)));
fields.insert("disabled".to_string(), QuillValue::from_json(json!(false)));
let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
assert_eq!(coerced.get("active").unwrap().as_i64().unwrap(), 1);
assert_eq!(coerced.get("disabled").unwrap().as_i64().unwrap(), 0);
}
#[test]
fn test_coerce_no_schema_properties() {
let schema = json!({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object"
});
let mut fields = HashMap::new();
fields.insert("title".to_string(), QuillValue::from_json(json!("Test")));
let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
assert_eq!(coerced.get("title").unwrap().as_str().unwrap(), "Test");
}
#[test]
fn test_coerce_field_without_type() {
let schema = json!({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object",
"properties": {
"title": {"description": "A title field"}
}
});
let mut fields = HashMap::new();
fields.insert("title".to_string(), QuillValue::from_json(json!("Test")));
let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
assert_eq!(coerced.get("title").unwrap().as_str().unwrap(), "Test");
}
#[test]
fn test_coerce_array_to_string() {
let schema = json!({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object",
"properties": {
"title": {"type": "string"},
"tags": {"type": "string"} }
});
let mut fields = HashMap::new();
fields.insert(
"title".to_string(),
QuillValue::from_json(json!(["Wrapped Title"])),
);
fields.insert(
"tags".to_string(),
QuillValue::from_json(json!(["tag1", "tag2"])),
);
let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
assert_eq!(
coerced.get("title").unwrap().as_str().unwrap(),
"Wrapped Title"
);
assert!(coerced.get("tags").unwrap().as_array().is_some());
assert_eq!(coerced.get("tags").unwrap().as_array().unwrap().len(), 2);
}
#[test]
fn test_coerce_mixed_fields() {
let schema = json!({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object",
"properties": {
"tags": {"type": "array"},
"active": {"type": "boolean"},
"count": {"type": "number"},
"title": {"type": "string"}
}
});
let mut fields = HashMap::new();
fields.insert("tags".to_string(), QuillValue::from_json(json!("single")));
fields.insert("active".to_string(), QuillValue::from_json(json!("true")));
fields.insert("count".to_string(), QuillValue::from_json(json!("42")));
fields.insert(
"title".to_string(),
QuillValue::from_json(json!("Test Title")),
);
let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
assert_eq!(coerced.get("tags").unwrap().as_array().unwrap().len(), 1);
assert!(coerced.get("active").unwrap().as_bool().unwrap());
assert_eq!(coerced.get("count").unwrap().as_i64().unwrap(), 42);
assert_eq!(
coerced.get("title").unwrap().as_str().unwrap(),
"Test Title"
);
}
#[test]
fn test_coerce_invalid_string_to_number() {
let schema = json!({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object",
"properties": {
"count": {"type": "number"}
}
});
let mut fields = HashMap::new();
fields.insert(
"count".to_string(),
QuillValue::from_json(json!("not-a-number")),
);
let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
assert_eq!(
coerced.get("count").unwrap().as_str().unwrap(),
"not-a-number"
);
}
#[test]
fn test_coerce_object_to_array() {
let schema = json!({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object",
"properties": {
"items": {"type": "array"}
}
});
let mut fields = HashMap::new();
fields.insert(
"items".to_string(),
QuillValue::from_json(json!({"key": "value"})),
);
let coerced = coerce_document(&QuillValue::from_json(schema), &fields);
let items = coerced.get("items").unwrap();
assert!(items.as_array().is_some());
let items_array = items.as_array().unwrap();
assert_eq!(items_array.len(), 1);
assert!(items_array[0].as_object().is_some());
}
#[test]
fn test_schema_card_in_defs() {
use crate::quill::CardSchema;
let fields = HashMap::new();
let mut cards = HashMap::new();
let name_schema = FieldSchema::new(
"name".to_string(),
FieldType::String,
Some("Name field".to_string()),
);
let mut card_fields = HashMap::new();
card_fields.insert("name".to_string(), name_schema);
let card = CardSchema {
name: "endorsements".to_string(),
title: Some("Endorsements".to_string()),
description: Some("Chain of endorsements".to_string()),
fields: card_fields,
ui: None,
};
cards.insert("endorsements".to_string(), card);
let document = CardSchema {
name: "root".to_string(),
title: None,
description: None,
fields,
ui: None,
};
let json_schema = build_schema(&document, &cards).unwrap().as_json().clone();
assert!(json_schema["$defs"].is_object());
assert!(json_schema["$defs"]["endorsements_card"].is_object());
let card_def = &json_schema["$defs"]["endorsements_card"];
assert_eq!(card_def["type"], "object");
assert_eq!(card_def["title"], "Endorsements");
assert_eq!(card_def["description"], "Chain of endorsements");
assert_eq!(card_def["properties"]["CARD"]["const"], "endorsements");
assert!(card_def["properties"]["name"].is_object());
assert_eq!(card_def["properties"]["name"]["type"], "string");
let required = card_def["required"].as_array().unwrap();
assert!(required.contains(&json!("CARD")));
}
#[test]
fn test_schema_cards_array() {
use crate::quill::CardSchema;
let fields = HashMap::new();
let mut cards = HashMap::new();
let mut name_schema = FieldSchema::new(
"name".to_string(),
FieldType::String,
Some("Endorser name".to_string()),
);
name_schema.required = true;
let mut org_schema = FieldSchema::new(
"org".to_string(),
FieldType::String,
Some("Organization".to_string()),
);
org_schema.default = Some(QuillValue::from_json(json!("Unknown")));
let mut card_fields = HashMap::new();
card_fields.insert("name".to_string(), name_schema);
card_fields.insert("org".to_string(), org_schema);
let card = CardSchema {
name: "endorsements".to_string(),
title: Some("Endorsements".to_string()),
description: Some("Chain of endorsements".to_string()),
fields: card_fields,
ui: None,
};
cards.insert("endorsements".to_string(), card);
let document = CardSchema {
name: "root".to_string(),
title: None,
description: None,
fields,
ui: None,
};
let json_schema = build_schema(&document, &cards).unwrap().as_json().clone();
let cards_prop = &json_schema["properties"]["CARDS"];
assert_eq!(cards_prop["type"], "array");
let items = &cards_prop["items"];
assert!(items["oneOf"].is_array());
let one_of = items["oneOf"].as_array().unwrap();
assert!(!one_of.is_empty());
assert_eq!(one_of[0]["$ref"], "#/$defs/endorsements_card");
assert!(items.get("x-discriminator").is_none());
let card_def = &json_schema["$defs"]["endorsements_card"];
assert_eq!(card_def["properties"]["name"]["type"], "string");
assert_eq!(card_def["properties"]["org"]["default"], "Unknown");
let required = card_def["required"].as_array().unwrap();
assert!(required.contains(&json!("CARD")));
assert!(required.contains(&json!("name")));
assert!(!required.contains(&json!("org")));
}
#[test]
fn test_extract_card_item_defaults() {
let schema = json!({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object",
"properties": {
"endorsements": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"org": { "type": "string", "default": "Unknown Org" },
"rank": { "type": "string", "default": "N/A" }
}
}
},
"title": { "type": "string" }
}
});
let card_defaults = extract_card_item_defaults(&QuillValue::from_json(schema));
assert_eq!(card_defaults.len(), 1);
assert!(card_defaults.contains_key("endorsements"));
let endorsements_defaults = card_defaults.get("endorsements").unwrap();
assert_eq!(endorsements_defaults.len(), 2); assert!(!endorsements_defaults.contains_key("name")); assert_eq!(
endorsements_defaults.get("org").unwrap().as_str(),
Some("Unknown Org")
);
assert_eq!(
endorsements_defaults.get("rank").unwrap().as_str(),
Some("N/A")
);
}
#[test]
fn test_extract_card_item_defaults_empty() {
let schema = json!({
"type": "object",
"properties": {
"title": { "type": "string" }
}
});
let card_defaults = extract_card_item_defaults(&QuillValue::from_json(schema));
assert!(card_defaults.is_empty());
}
#[test]
fn test_extract_card_item_defaults_no_item_defaults() {
let schema = json!({
"type": "object",
"properties": {
"endorsements": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"org": { "type": "string" }
}
}
}
}
});
let card_defaults = extract_card_item_defaults(&QuillValue::from_json(schema));
assert!(card_defaults.is_empty()); }
#[test]
fn test_apply_card_item_defaults() {
let mut item_defaults = HashMap::new();
item_defaults.insert(
"org".to_string(),
QuillValue::from_json(json!("Default Org")),
);
let mut card_defaults = HashMap::new();
card_defaults.insert("endorsements".to_string(), item_defaults);
let mut fields = HashMap::new();
fields.insert(
"endorsements".to_string(),
QuillValue::from_json(json!([
{ "name": "John Doe" },
{ "name": "Jane Smith", "org": "Custom Org" }
])),
);
let result = apply_card_item_defaults(&fields, &card_defaults);
let endorsements = result.get("endorsements").unwrap().as_array().unwrap();
assert_eq!(endorsements.len(), 2);
assert_eq!(endorsements[0]["name"], "John Doe");
assert_eq!(endorsements[0]["org"], "Default Org");
assert_eq!(endorsements[1]["name"], "Jane Smith");
assert_eq!(endorsements[1]["org"], "Custom Org");
}
#[test]
fn test_apply_card_item_defaults_empty_card() {
let mut item_defaults = HashMap::new();
item_defaults.insert(
"org".to_string(),
QuillValue::from_json(json!("Default Org")),
);
let mut card_defaults = HashMap::new();
card_defaults.insert("endorsements".to_string(), item_defaults);
let mut fields = HashMap::new();
fields.insert("endorsements".to_string(), QuillValue::from_json(json!([])));
let result = apply_card_item_defaults(&fields, &card_defaults);
let endorsements = result.get("endorsements").unwrap().as_array().unwrap();
assert!(endorsements.is_empty());
}
#[test]
fn test_apply_card_item_defaults_no_matching_card() {
let mut item_defaults = HashMap::new();
item_defaults.insert(
"org".to_string(),
QuillValue::from_json(json!("Default Org")),
);
let mut card_defaults = HashMap::new();
card_defaults.insert("endorsements".to_string(), item_defaults);
let mut fields = HashMap::new();
fields.insert(
"reviews".to_string(),
QuillValue::from_json(json!([{ "author": "Bob" }])),
);
let result = apply_card_item_defaults(&fields, &card_defaults);
let reviews = result.get("reviews").unwrap().as_array().unwrap();
assert_eq!(reviews.len(), 1);
assert_eq!(reviews[0]["author"], "Bob");
assert!(reviews[0].get("org").is_none());
}
#[test]
fn test_card_validation_with_required_fields() {
let schema = json!({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object",
"properties": {
"endorsements": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"org": { "type": "string", "default": "Unknown" }
},
"required": ["name"]
}
}
}
});
let mut valid_fields = HashMap::new();
valid_fields.insert(
"endorsements".to_string(),
QuillValue::from_json(json!([{ "name": "John" }])),
);
let result = validate_document(&QuillValue::from_json(schema.clone()), &valid_fields);
assert!(result.is_ok());
let mut invalid_fields = HashMap::new();
invalid_fields.insert(
"endorsements".to_string(),
QuillValue::from_json(json!([{ "org": "SomeOrg" }])),
);
let result = validate_document(&QuillValue::from_json(schema), &invalid_fields);
assert!(result.is_err());
}
#[test]
fn test_validate_document_invalid_card_type() {
use crate::quill::{CardSchema, FieldSchema};
let mut card_fields = HashMap::new();
card_fields.insert(
"field1".to_string(),
FieldSchema::new(
"f1".to_string(),
FieldType::String,
Some("desc".to_string()),
),
);
let mut card_schemas = HashMap::new();
card_schemas.insert(
"valid_card".to_string(),
CardSchema {
name: "valid_card".to_string(),
title: None,
description: None,
fields: card_fields,
ui: None,
},
);
let document = CardSchema {
name: "root".to_string(),
title: None,
description: None,
fields: HashMap::new(),
ui: None,
};
let schema = build_schema(&document, &card_schemas).unwrap();
let mut fields = HashMap::new();
let invalid_card = json!({
"CARD": "invalid_type",
"field1": "value" });
fields.insert(
"CARDS".to_string(),
QuillValue::from_json(json!([invalid_card])),
);
let result = validate_document(&QuillValue::from_json(schema.as_json().clone()), &fields);
assert!(result.is_err());
let errs = result.unwrap_err();
let err_msg = &errs[0];
assert!(err_msg.contains("Invalid card type 'invalid_type'"));
assert!(err_msg.contains("Valid types are: [valid_card]"));
}
#[test]
fn test_coerce_document_cards() {
let mut card_fields = HashMap::new();
let count_schema = FieldSchema::new(
"Count".to_string(),
FieldType::Number,
Some("A number".to_string()),
);
card_fields.insert("count".to_string(), count_schema);
let active_schema = FieldSchema::new(
"Active".to_string(),
FieldType::Boolean,
Some("A boolean".to_string()),
);
card_fields.insert("active".to_string(), active_schema);
let mut card_schemas = HashMap::new();
card_schemas.insert(
"test_card".to_string(),
CardSchema {
name: "test_card".to_string(),
title: None,
description: Some("Test card".to_string()),
fields: card_fields,
ui: None,
},
);
let document = CardSchema {
name: "root".to_string(),
title: None,
description: None,
fields: HashMap::new(),
ui: None,
};
let schema = build_schema(&document, &card_schemas).unwrap();
let mut fields = HashMap::new();
let card_value = json!({
"CARD": "test_card",
"count": "42",
"active": "true"
});
fields.insert(
"CARDS".to_string(),
QuillValue::from_json(json!([card_value])),
);
let coerced_fields = coerce_document(&schema, &fields);
let cards_array = coerced_fields.get("CARDS").unwrap().as_array().unwrap();
let coerced_card = cards_array[0].as_object().unwrap();
assert_eq!(coerced_card.get("count").unwrap().as_i64(), Some(42));
assert_eq!(coerced_card.get("active").unwrap().as_bool(), Some(true));
}
#[test]
fn test_validate_document_card_fields() {
let mut card_fields = HashMap::new();
let count_schema = FieldSchema::new(
"Count".to_string(),
FieldType::Number,
Some("A number".to_string()),
);
card_fields.insert("count".to_string(), count_schema);
let mut card_schemas = HashMap::new();
card_schemas.insert(
"test_card".to_string(),
CardSchema {
name: "test_card".to_string(),
title: None,
description: Some("Test card".to_string()),
fields: card_fields,
ui: None,
},
);
let document = CardSchema {
name: "root".to_string(),
title: None,
description: None,
fields: HashMap::new(),
ui: None,
};
let schema = build_schema(&document, &card_schemas).unwrap();
let mut fields = HashMap::new();
let card_value = json!({
"CARD": "test_card",
"count": "not a number" });
fields.insert(
"CARDS".to_string(),
QuillValue::from_json(json!([card_value])),
);
let result = validate_document(&QuillValue::from_json(schema.as_json().clone()), &fields);
assert!(result.is_err());
let errs = result.unwrap_err();
let found_specific_error = errs
.iter()
.any(|e| e.contains("/CARDS/0") && e.contains("not a number") && !e.contains("oneOf"));
assert!(
found_specific_error,
"Did not find specific error msg in: {:?}",
errs
);
}
#[test]
fn test_card_field_ui_metadata() {
use crate::quill::{CardSchema, UiFieldSchema};
let mut field_schema = FieldSchema::new(
"from".to_string(),
FieldType::String,
Some("Sender".to_string()),
);
field_schema.ui = Some(UiFieldSchema {
group: Some("Header".to_string()),
order: Some(0),
visible_when: None,
});
let mut card_fields = HashMap::new();
card_fields.insert("from".to_string(), field_schema);
let card = CardSchema {
name: "indorsement".to_string(),
title: Some("Indorsement".to_string()),
description: Some("An indorsement".to_string()),
fields: card_fields,
ui: None,
};
let mut cards = HashMap::new();
cards.insert("indorsement".to_string(), card);
let document = CardSchema {
name: "root".to_string(),
title: None,
description: None,
fields: HashMap::new(),
ui: None,
};
let schema = build_schema(&document, &cards).unwrap();
let card_def = &schema.as_json()["$defs"]["indorsement_card"];
let from_field = &card_def["properties"]["from"];
assert_eq!(from_field["x-ui"]["group"], "Header");
assert_eq!(from_field["x-ui"]["order"], 0);
}
#[test]
fn test_hide_body_schema() {
use crate::quill::{CardSchema, UiContainerSchema};
let ui_schema = UiContainerSchema {
hide_body: Some(true),
};
let field_schema = FieldSchema::new(
"name".to_string(),
FieldType::String,
Some("Name".to_string()),
);
let mut card_fields = HashMap::new();
card_fields.insert("name".to_string(), field_schema);
let card = CardSchema {
name: "meta_card".to_string(),
title: None,
description: Some("Meta only card".to_string()),
fields: card_fields,
ui: Some(UiContainerSchema {
hide_body: Some(true),
}),
};
let mut cards = HashMap::new();
cards.insert("meta_card".to_string(), card);
let document = CardSchema {
name: "root".to_string(),
title: None,
description: None,
fields: HashMap::new(),
ui: Some(ui_schema),
};
let schema = build_schema(&document, &cards).unwrap();
let json_schema = schema.as_json();
assert!(json_schema.get("x-ui").is_some());
assert_eq!(json_schema["x-ui"]["hide_body"], true);
let card_def = &json_schema["$defs"]["meta_card_card"];
assert!(card_def.get("x-ui").is_some(), "Card should have x-ui");
assert_eq!(card_def["x-ui"]["hide_body"], true);
}
#[test]
fn test_visible_when_schema() {
use crate::quill::{CardSchema, UiFieldSchema};
let mut field_with_condition = FieldSchema::new(
"from".to_string(),
FieldType::String,
Some("Sender".to_string()),
);
let mut visible_when = HashMap::new();
visible_when.insert(
"format".to_string(),
vec!["standard".to_string(), "separate_page".to_string()],
);
field_with_condition.ui = Some(UiFieldSchema {
group: Some("Addressing".to_string()),
order: Some(0),
visible_when: Some(visible_when),
});
let mut format_field = FieldSchema::new(
"format".to_string(),
FieldType::String,
Some("Format".to_string()),
);
format_field.enum_values = Some(vec![
"standard".to_string(),
"informal".to_string(),
"separate_page".to_string(),
]);
format_field.ui = Some(UiFieldSchema {
group: Some("Additional".to_string()),
order: Some(1),
visible_when: None,
});
let mut card_fields = HashMap::new();
card_fields.insert("from".to_string(), field_with_condition);
card_fields.insert("format".to_string(), format_field);
let card = CardSchema {
name: "indorsement".to_string(),
title: Some("Indorsement".to_string()),
description: Some("An indorsement".to_string()),
fields: card_fields,
ui: None,
};
let mut cards = HashMap::new();
cards.insert("indorsement".to_string(), card);
let document = CardSchema {
name: "root".to_string(),
title: None,
description: None,
fields: HashMap::new(),
ui: None,
};
let schema = build_schema(&document, &cards).unwrap();
let card_def = &schema.as_json()["$defs"]["indorsement_card"];
let from_field = &card_def["properties"]["from"];
let format_field = &card_def["properties"]["format"];
let vw = &from_field["x-ui"]["visible_when"];
assert!(vw.is_object(), "visible_when should be an object");
let format_values = vw["format"].as_array().unwrap();
assert_eq!(format_values.len(), 2);
assert!(format_values.contains(&json!("standard")));
assert!(format_values.contains(&json!("separate_page")));
assert!(
format_field["x-ui"].get("visible_when").is_none()
|| format_field["x-ui"]["visible_when"].is_null(),
"format field should not have visible_when"
);
}
#[test]
fn test_visible_when_stripped() {
use crate::quill::UiFieldSchema;
let mut field = FieldSchema::new(
"from".to_string(),
FieldType::String,
Some("Sender".to_string()),
);
let mut visible_when = HashMap::new();
visible_when.insert("format".to_string(), vec!["standard".to_string()]);
field.ui = Some(UiFieldSchema {
group: Some("Addressing".to_string()),
order: Some(0),
visible_when: Some(visible_when),
});
let mut fields = HashMap::new();
fields.insert("from".to_string(), field);
let document = CardSchema {
name: "root".to_string(),
title: None,
description: None,
fields,
ui: None,
};
let schema = build_schema(&document, &HashMap::new()).unwrap();
let mut schema_json = schema.as_json().clone();
assert!(schema_json["properties"]["from"].get("x-ui").is_some());
strip_schema_fields(&mut schema_json, &["x-ui"]);
assert!(schema_json["properties"]["from"].get("x-ui").is_none());
}
#[test]
fn test_visible_when_yaml_roundtrip() {
let yaml = r#"
Quill:
name: test_vw
version: "0.1"
backend: typst
description: Test visible_when
fields:
format:
type: string
enum:
- standard
- informal
default: standard
cards:
endorsement:
title: Endorsement
description: Test card
fields:
from:
type: string
ui:
group: Addressing
visible_when:
format: [standard]
note:
type: string
"#;
let config = crate::quill::QuillConfig::from_yaml(yaml).unwrap();
let card = config.cards.get("endorsement").unwrap();
let from_field = card.fields.get("from").unwrap();
let ui = from_field.ui.as_ref().unwrap();
let vw = ui.visible_when.as_ref().unwrap();
assert_eq!(vw.get("format").unwrap(), &vec!["standard".to_string()]);
let schema = build_schema(&config.document, &config.cards).unwrap();
let card_def = &schema.as_json()["$defs"]["endorsement_card"];
let from_prop = &card_def["properties"]["from"];
assert_eq!(from_prop["x-ui"]["visible_when"]["format"][0], "standard");
let note_prop = &card_def["properties"]["note"];
assert!(
note_prop["x-ui"].get("visible_when").is_none()
|| note_prop["x-ui"]["visible_when"].is_null()
);
}
}