RustyCMS: file-based headless CMS — API, Admin UI (content, types, assets), Docker/Caddy, image transform; only demo type and demo content in version control

Made-with: Cursor
This commit is contained in:
Peter Meier
2026-03-12 14:21:49 +01:00
parent aad93d145f
commit 7795a238e1
278 changed files with 15551 additions and 4072 deletions

View File

@@ -108,13 +108,14 @@ fn validate_field(
// ── Type check ──────────────────────────────────────────────────────
let type_ok = match fd.field_type.as_str() {
"string" | "richtext" | "html" | "markdown" | "datetime" => value.is_string(),
"string" | "richtext" | "html" | "markdown" | "datetime" | "textOrRef" => value.is_string(),
"number" => value.is_number(),
"integer" => value.is_i64() || value.is_u64() || value.as_f64().map(|f| f.fract() == 0.0 && f.is_finite()).unwrap_or(false),
"boolean" => value.is_boolean(),
"array" => value.is_array(),
"object" => value.is_object(),
"reference" => value.is_string(),
"referenceOrInline" => value.is_string() || value.is_object(),
_ => true,
};
@@ -222,17 +223,24 @@ fn validate_field(
}
// ── Nested object validation ────────────────────────────────────────
if fd.field_type == "object" {
if let (Some(ref nested_fields), Some(obj)) = (&fd.fields, value.as_object()) {
for (nested_name, nested_def) in nested_fields {
let full_name = format!("{}.{}", field_name, nested_name);
if let Some(nested_value) = obj.get(nested_name) {
validate_field(&full_name, nested_def, nested_value, errors);
} else if nested_def.required {
errors.push(ValidationError {
field: full_name,
message: "Field is required".to_string(),
});
if fd.field_type == "object" || fd.field_type == "referenceOrInline" {
if value.is_object() {
if let (Some(ref nested_fields), Some(obj)) = (&fd.fields, value.as_object()) {
for (nested_name, nested_def) in nested_fields {
let full_name = format!("{}.{}", field_name, nested_name);
if let Some(nested_value) = obj.get(nested_name) {
validate_field(&full_name, nested_def, nested_value, errors);
} else if nested_def.required {
errors.push(ValidationError {
field: full_name,
message: "Field is required".to_string(),
});
}
}
} else if let (Some(ref ap), Some(obj)) = (&fd.additional_properties, value.as_object()) {
for (key, val) in obj {
let full_name = format!("{}.{}", field_name, key);
validate_field(&full_name, ap, val, errors);
}
}
}
@@ -277,6 +285,61 @@ pub fn apply_defaults(schema: &SchemaDefinition, content: &mut Value) {
}
}
// ---------------------------------------------------------------------------
// Normalize reference arrays (used before validation on create/update)
// ---------------------------------------------------------------------------
/// Convert reference-array items from objects `{ _slug, ... }` to slug strings.
/// So clients can send either slugs or resolved refs; we always store slugs.
pub fn normalize_reference_arrays(schema: &SchemaDefinition, content: &mut Value) {
let obj = match content.as_object_mut() {
Some(o) => o,
None => return,
};
for (field_name, fd) in &schema.fields {
// Single reference / referenceOrInline: object with _slug → slug string
if fd.field_type == "reference" || fd.field_type == "referenceOrInline" {
let slug_opt = obj
.get(field_name)
.and_then(|v| v.as_object())
.and_then(|o| o.get("_slug"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if let Some(s) = slug_opt {
obj.insert(field_name.clone(), Value::String(s));
}
continue;
}
if fd.field_type != "array" {
continue;
}
let items = match &fd.items {
Some(it) => it,
None => continue,
};
let is_ref_array =
items.field_type == "reference" || items.field_type == "referenceOrInline";
if !is_ref_array {
continue;
}
let arr = match obj.get_mut(field_name).and_then(|v| v.as_array_mut()) {
Some(a) => a,
None => continue,
};
for item in arr.iter_mut() {
let slug_opt = item
.as_object()
.and_then(|o| o.get("_slug"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if let Some(s) = slug_opt {
*item = Value::String(s);
}
}
}
}
// ---------------------------------------------------------------------------
// Readonly check (used by update handler)
// ---------------------------------------------------------------------------
@@ -387,7 +450,9 @@ pub fn validate_references(
if let Some(obj) = content.as_object() {
for (field_name, fd) in &schema.fields {
if fd.field_type == "reference" {
let is_ref = fd.field_type == "reference"
|| (fd.field_type == "referenceOrInline" && obj.get(field_name).map_or(false, |v| v.is_string()));
if is_ref {
let colls = fd.reference_collections();
if colls.is_empty() {
continue;
@@ -408,7 +473,9 @@ pub fn validate_references(
}
if fd.field_type == "array" {
if let Some(ref items) = fd.items {
if items.field_type != "reference" {
let is_ref_item = items.field_type == "reference"
|| items.field_type == "referenceOrInline";
if !is_ref_item {
continue;
}
let colls = items.reference_collections();