90 lines
2.8 KiB
Text
90 lines
2.8 KiB
Text
# Content Item Metadata Contracts
|
|
#
|
|
# Primitive validation contracts for post metadata fields.
|
|
# Imported by post_metadata.ncl and recipe_metadata.ncl.
|
|
#
|
|
# All contracts are | not_exported — accessible via import but excluded
|
|
# from JSON export. Nickel evaluates contracts at validation time,
|
|
# not at export time, so this is safe.
|
|
|
|
{
|
|
SlugContract
|
|
| not_exported
|
|
| doc "URL-safe slug: lowercase letters, digits, and hyphens only. No leading/trailing hyphens."
|
|
= std.contract.custom (
|
|
fun label =>
|
|
fun value =>
|
|
if std.string.is_match "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$" value then
|
|
'Ok value
|
|
else
|
|
'Error {
|
|
message = "Invalid slug '%{value}'.\nValid: lowercase letters, digits, hyphens only. E.g. 'my-rust-post-2024'"
|
|
}
|
|
),
|
|
|
|
DateContract
|
|
| not_exported
|
|
| doc "ISO 8601 calendar date only — no time component."
|
|
= std.contract.custom (
|
|
fun label =>
|
|
fun value =>
|
|
if std.string.is_match "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$" value then
|
|
'Ok value
|
|
else
|
|
'Error {
|
|
message = "Invalid date '%{value}'.\nValid format: YYYY-MM-DD (e.g. '2024-01-15')"
|
|
}
|
|
),
|
|
|
|
NonEmptyString
|
|
| not_exported
|
|
| doc "Rejects empty strings. Allows any non-empty content."
|
|
= std.contract.custom (
|
|
fun label =>
|
|
fun value =>
|
|
if (value |> std.string.length) > 0 then
|
|
'Ok value
|
|
else
|
|
'Error {
|
|
message = "Invalid value: empty string not allowed.\nProvide at least one character."
|
|
}
|
|
),
|
|
|
|
DifficultyLevel
|
|
| not_exported
|
|
| doc "Skill level required for recipes and tutorials."
|
|
= std.contract.custom (
|
|
fun label =>
|
|
fun value =>
|
|
if std.array.elem value ["beginner", "intermediate", "advanced"] then
|
|
'Ok value
|
|
else
|
|
'Error {
|
|
message = "Invalid difficulty '%{value}'.\nValid values: beginner | intermediate | advanced"
|
|
}
|
|
),
|
|
|
|
ReadTimeContract
|
|
| not_exported
|
|
| doc "Human-readable duration string. Must contain a digit."
|
|
= std.contract.custom (
|
|
fun label =>
|
|
fun value =>
|
|
if (std.string.is_match "[0-9]" value) && ((std.string.length value) > 0) then
|
|
'Ok value
|
|
else
|
|
'Error {
|
|
message = "Invalid read_time '%{value}'.\nMust contain a number (e.g. '8 min read', '2 hours')"
|
|
}
|
|
),
|
|
|
|
TranslationEntry = {
|
|
id
|
|
| String
|
|
| doc "Stable identifier of the translated post (shared across all language versions).",
|
|
|
|
slug
|
|
| String
|
|
| doc "URL slug of the translated post in its target language.",
|
|
},
|
|
}
|