use serde_json::{json, Value};
use std::fs;
use std::io;
use std::path::Path;
#[must_use]
pub fn generate_schema() -> Value {
json!({
"$schema": "https://json-schema.org/draft-07/schema#",
"title": "SsgConfig",
"description": "Configuration for the Static Site Generator (SSG).",
"type": "object",
"properties": {
"site_name": {
"type": "string",
"description": "Name of the site.",
"default": "MySsgSite"
},
"content_dir": {
"type": "string",
"description": "Directory containing content files.",
"default": "content"
},
"output_dir": {
"type": "string",
"description": "Directory for generated output files.",
"default": "public"
},
"template_dir": {
"type": "string",
"description": "Directory containing template files.",
"default": "templates"
},
"serve_dir": {
"type": ["string", "null"],
"description": "Optional directory for development server files.",
"default": null
},
"base_url": {
"type": "string",
"description": "Base URL of the site.",
"default": "http://127.0.0.1:8000",
"format": "uri"
},
"site_title": {
"type": "string",
"description": "Title of the site.",
"default": "My SSG Site"
},
"site_description": {
"type": "string",
"description": "Description of the site.",
"default": "A site built with SSG"
},
"language": {
"type": "string",
"description": "Language code for the site (e.g. en-GB).",
"default": "en-GB",
"pattern": "^[a-z]{2}-[A-Z]{2}$"
}
},
"required": [
"site_name",
"content_dir",
"output_dir",
"template_dir",
"base_url",
"site_title",
"site_description",
"language"
],
"additionalProperties": false
})
}
pub fn write_schema(path: &Path) -> io::Result<()> {
let schema = generate_schema();
#[allow(clippy::expect_used)]
let content = serde_json::to_string_pretty(&schema)
.expect("hand-authored Schema is always serializable");
fs::write(path, content)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use std::path::PathBuf;
use tempfile::tempdir;
#[test]
fn schema_has_correct_title() {
let schema = generate_schema();
assert_eq!(schema["title"], "SsgConfig");
}
#[test]
fn schema_has_all_required_fields() {
let schema = generate_schema();
let required = schema["required"]
.as_array()
.expect("required should be an array");
let names: Vec<&str> =
required.iter().map(|v| v.as_str().unwrap()).collect();
assert!(names.contains(&"site_name"));
assert!(names.contains(&"content_dir"));
assert!(names.contains(&"output_dir"));
assert!(names.contains(&"template_dir"));
assert!(names.contains(&"base_url"));
assert!(names.contains(&"site_title"));
assert!(names.contains(&"site_description"));
assert!(names.contains(&"language"));
}
#[test]
fn schema_properties_have_types() {
let schema = generate_schema();
let props = schema["properties"]
.as_object()
.expect("properties should be an object");
for (key, value) in props {
assert!(
value.get("type").is_some(),
"property '{key}' is missing a type"
);
}
}
#[test]
fn schema_defaults_match_config() {
let schema = generate_schema();
let props = &schema["properties"];
assert_eq!(props["site_name"]["default"], "MySsgSite");
assert_eq!(props["content_dir"]["default"], "content");
assert_eq!(props["output_dir"]["default"], "public");
assert_eq!(props["template_dir"]["default"], "templates");
assert_eq!(props["base_url"]["default"], "http://127.0.0.1:8000");
assert_eq!(props["site_title"]["default"], "My SSG Site");
assert_eq!(
props["site_description"]["default"],
"A site built with SSG"
);
assert_eq!(props["language"]["default"], "en-GB");
}
#[test]
fn schema_language_has_pattern() {
let schema = generate_schema();
let pattern = schema["properties"]["language"]["pattern"]
.as_str()
.expect("language should have a pattern");
assert_eq!(pattern, "^[a-z]{2}-[A-Z]{2}$");
}
#[test]
fn serve_dir_allows_null() {
let schema = generate_schema();
let types = schema["properties"]["serve_dir"]["type"]
.as_array()
.expect("serve_dir type should be an array");
let type_strs: Vec<&str> =
types.iter().map(|v| v.as_str().unwrap()).collect();
assert!(type_strs.contains(&"null"));
assert!(type_strs.contains(&"string"));
}
#[test]
fn write_schema_creates_valid_json_file() {
let dir = tempdir().expect("failed to create temp dir");
let path = dir.path().join("schema.json");
write_schema(&path).expect("write_schema failed");
let content =
fs::read_to_string(&path).expect("failed to read schema file");
let parsed: Value =
serde_json::from_str(&content).expect("output is not valid JSON");
assert_eq!(parsed["title"], "SsgConfig");
}
#[test]
fn write_schema_fails_on_bad_path() {
let path = PathBuf::from("/nonexistent/dir/schema.json");
assert!(write_schema(&path).is_err());
}
}