use hyle::{
Blueprint, Field, FieldType, Forma, FormaContext, FormaField, FormaFieldType, Model,
ModelResult, Outcome, Primitive, Query, Row, ShapeField, Source, Value,
};
use indexmap::IndexMap;
use serde_json::json;
fn blueprint() -> Blueprint {
Blueprint::new()
.model(
"user",
Model::new()
.field("name", Field::string("Name"))
.field("email", Field::string("Email"))
.field("role", Field::reference("Role", "role"))
.field("active", Field::boolean("Active")),
)
.model(
"role",
Model::new().field("name", Field::string("Role name")),
)
}
fn two_named_rows() -> Vec<Row> {
vec![
IndexMap::from([("name".to_owned(), json!("Alice"))]),
IndexMap::from([("name".to_owned(), json!("Bruno"))]),
]
}
#[test]
fn derives_query_plan_with_selected_fields_and_enum_dependencies() {
let plan = blueprint()
.manifest(
Query::new("user")
.select(["name", "role", "active"])
.filter_layout(vec![vec!["name", "role"], vec!["active"]])
.where_eq("active", json!(true))
.page(2, 25)
.sort_by("name", true),
)
.unwrap();
assert_eq!(plan.base, "user");
assert_eq!(plan.fields, vec!["name", "role", "active"]);
assert_eq!(plan.filter["active"], json!(true));
assert_eq!(plan.lookups, vec!["role"]);
assert!(plan.inlines.is_empty());
assert_eq!(plan.page, Some(2));
assert_eq!(plan.per_page, Some(25));
assert_eq!(plan.sort.unwrap().field, "name");
}
#[test]
fn marks_selected_references_as_joins_when_not_used_as_filters() {
let plan = blueprint()
.manifest(Query::new("user").select(["name", "role"]))
.unwrap();
assert!(plan.lookups.is_empty());
assert_eq!(plan.inlines, vec!["role"]);
}
#[test]
fn resolves_raw_data_into_rows_and_lookup_maps() {
let blueprint = blueprint();
let plan = blueprint
.manifest(Query::new("user").select(["name", "role"]))
.unwrap();
let mut user = IndexMap::new();
user.insert("id".to_owned(), Value::from(1));
user.insert("name".to_owned(), Value::from("Alice"));
user.insert("role".to_owned(), Value::from("admin"));
let mut role = IndexMap::new();
role.insert("id".to_owned(), Value::from("admin"));
role.insert("name".to_owned(), Value::from("Admin"));
let mut raw = Source::new();
raw.insert("user".to_owned(), ModelResult::many(vec![user]));
raw.insert("role".to_owned(), ModelResult::many(vec![role]));
let resolved = blueprint.resolve(&plan, &raw).unwrap();
assert_eq!(resolved.total, 1);
assert_eq!(resolved.lookups["role"]["admin"]["name"], json!("Admin"));
}
#[test]
fn serializes_to_js_friendly_json() {
let blueprint = blueprint();
let plan = blueprint
.manifest(Query::new("user").select(["name", "email"]))
.unwrap();
let json = serde_json::to_value(plan).unwrap();
assert_eq!(json["base"], "user");
assert_eq!(json["fields"], json!(["name", "email"]));
}
#[test]
fn filters_rows_using_manifest_filter_semantics() {
let rows = vec![
IndexMap::from([
("name".to_owned(), json!("Alice")),
("active".to_owned(), json!(true)),
]),
IndexMap::from([
("name".to_owned(), json!("Bruno")),
("active".to_owned(), json!(false)),
]),
];
let filters = IndexMap::from([("name".to_owned(), json!("ali"))]);
let filtered = hyle::filter_rows(&rows, &filters);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0]["name"], json!("Alice"));
}
#[test]
fn displays_reference_values_from_lookup_tables() {
let blueprint = blueprint();
let plan = blueprint
.manifest(Query::new("user").select(["name", "role"]))
.unwrap();
let user = IndexMap::from([
("id".to_owned(), json!(1)),
("name".to_owned(), json!("Alice")),
("role".to_owned(), json!("admin")),
]);
let role = IndexMap::from([
("id".to_owned(), json!("admin")),
("name".to_owned(), json!("Admin")),
]);
let mut source = Source::new();
source.insert("user".to_owned(), ModelResult::many(vec![user]));
source.insert("role".to_owned(), ModelResult::many(vec![role]));
let outcome = blueprint.resolve(&plan, &source).unwrap();
let displayed = hyle::display_value(&blueprint, &outcome, "user", "role", &json!("admin"));
assert_eq!(displayed, "Admin");
}
#[test]
fn plan_selects_all_fields_when_select_is_empty() {
let manifest = blueprint().manifest(Query::new("user")).unwrap();
assert_eq!(manifest.fields, vec!["name", "email", "role", "active"]);
}
#[test]
fn plan_errors_on_unknown_model() {
let result = blueprint().manifest(Query::new("ghost"));
assert!(result.is_err());
}
#[test]
fn plan_errors_on_unknown_field_in_select() {
let result = blueprint().manifest(Query::new("user").select(["name", "ghost"]));
assert!(result.is_err());
}
#[test]
fn plan_errors_on_unknown_field_in_filter_layout() {
let result = blueprint().manifest(
Query::new("user")
.select(["name"])
.filter_layout([["ghost"]]),
);
assert!(result.is_err());
}
#[test]
fn plan_errors_on_unknown_reference_target() {
let bp = Blueprint::new().model(
"m",
Model::new().field("x", Field::reference("X", "nonexistent")),
);
let result = bp.manifest(Query::new("m").select(["x"]));
assert!(result.is_err());
}
#[test]
fn resolve_errors_when_base_model_missing_from_source() {
let blueprint = blueprint();
let manifest = blueprint.manifest(Query::new("user").select(["name"])).unwrap();
let result = blueprint.resolve(&manifest, &Source::new());
assert!(result.is_err());
}
#[test]
fn resolve_query_returns_manifest_outcome_and_rows() {
let blueprint = blueprint();
let user = IndexMap::from([
("id".to_owned(), json!(1)),
("name".to_owned(), json!("Alice")),
]);
let mut source = Source::new();
source.insert("user".to_owned(), ModelResult::many(vec![user]));
let (manifest, outcome, rows) = blueprint
.resolve_query(Query::new("user").select(["name"]), &source)
.unwrap();
assert_eq!(manifest.base, "user");
assert_eq!(outcome.total, 1);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["name"], json!("Alice"));
}
#[test]
fn display_value_formats_boolean_as_yes_no() {
let blueprint = blueprint();
let outcome = Outcome::empty();
assert_eq!(
hyle::display_value(&blueprint, &outcome, "user", "active", &json!(true)),
"Yes"
);
assert_eq!(
hyle::display_value(&blueprint, &outcome, "user", "active", &json!(false)),
"No"
);
}
#[test]
fn display_value_returns_empty_string_for_null() {
let blueprint = blueprint();
let outcome = Outcome::empty();
assert_eq!(
hyle::display_value(&blueprint, &outcome, "user", "name", &json!(null)),
""
);
}
#[test]
fn display_value_falls_back_for_unknown_field() {
let blueprint = blueprint();
let outcome = Outcome::empty();
assert_eq!(
hyle::display_value(&blueprint, &outcome, "user", "nonexistent", &json!("raw")),
"raw"
);
}
#[test]
fn display_value_renders_array_as_comma_separated() {
let bp = Blueprint::new().model(
"item",
Model::new().field(
"tags",
Field::array(
"Tags",
FieldType::Primitive {
primitive: Primitive::String,
},
),
),
);
let outcome = Outcome::empty();
let displayed = hyle::display_value(&bp, &outcome, "item", "tags", &json!(["a", "b", "c"]));
assert_eq!(displayed, "a, b, c");
}
#[test]
fn display_value_renders_shape_as_label_colon_value() {
let mut shape_fields = IndexMap::new();
shape_fields.insert(
"street".to_owned(),
ShapeField::new("Street", FieldType::Primitive { primitive: Primitive::String }),
);
shape_fields.insert(
"city".to_owned(),
ShapeField::new("City", FieldType::Primitive { primitive: Primitive::String }),
);
let bp = Blueprint::new().model(
"contact",
Model::new().field("address", Field::shape("Address", shape_fields)),
);
let outcome = Outcome::empty();
let displayed = hyle::display_value(
&bp,
&outcome,
"contact",
"address",
&json!({ "street": "123 Main St", "city": "Springfield" }),
);
assert_eq!(displayed, "Street: 123 Main St; City: Springfield");
}
#[test]
fn filter_rows_ignores_empty_string_filter() {
let rows = two_named_rows();
let filters = IndexMap::from([("name".to_owned(), json!(""))]);
let filtered = hyle::filter_rows(&rows, &filters);
assert_eq!(filtered.len(), 2);
}
#[test]
fn filter_rows_ignores_null_filter() {
let rows = two_named_rows();
let filters = IndexMap::from([("name".to_owned(), json!(null))]);
let filtered = hyle::filter_rows(&rows, &filters);
assert_eq!(filtered.len(), 2);
}
#[test]
fn model_result_one_has_total_of_one() {
let row: Row = IndexMap::from([("id".to_owned(), json!(42))]);
let mr = ModelResult::one(row.clone());
assert_eq!(mr.total, 1);
assert_eq!(mr.rows(), vec![row]);
}
#[test]
fn purify_enforces_numeric_max() {
let bp = Blueprint::new().model(
"m",
Model::new().field("score", Field::number("Score").with_metadata("max", json!(10))),
);
let row = IndexMap::from([("score".to_owned(), json!(11))]);
let result = hyle::purify_row_sync(&bp, "m", &row);
assert!(result.is_err());
assert_eq!(result.unwrap_err()[0].rule, "max");
}
#[test]
fn purify_enforces_max_length() {
let bp = Blueprint::new().model(
"m",
Model::new()
.field("code", Field::string("Code").with_metadata("maxLength", json!(3))),
);
let row = IndexMap::from([("code".to_owned(), json!("toolong"))]);
let result = hyle::purify_row_sync(&bp, "m", &row);
assert!(result.is_err());
assert_eq!(result.unwrap_err()[0].rule, "maxLength");
}
#[test]
fn forma_to_query_uses_form_context_fields() {
let forma = Forma {
fields: vec![
FormaField { name: "name".into(), label: "Name".into(), ..Default::default() },
FormaField { name: "role".into(), label: "Role".into(), ..Default::default() },
],
form: Some(vec!["name".into()]),
..Default::default()
};
let query = hyle::forma_to_query(&forma, "user", &FormaContext::Form, None);
assert_eq!(query.select, vec!["name"]);
}
#[test]
fn forma_to_query_seeds_filters_from_forma() {
let forma = Forma {
fields: vec![
FormaField { name: "name".into(), label: "Name".into(), ..Default::default() },
FormaField { name: "role".into(), label: "Role".into(), ..Default::default() },
],
filters: Some(vec![vec!["name".into(), "role".into()]]),
..Default::default()
};
let query = hyle::forma_to_query(&forma, "user", &FormaContext::Column, None);
assert_eq!(query.filters, vec![vec!["name", "role"]]);
let manifest = blueprint().manifest(query).unwrap();
assert_eq!(manifest.filter_fields, vec![vec!["name", "role"]]);
}