# 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.",
  },
}
