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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user