1use serde_json::{json, Value};
11use std::fs;
12use std::io;
13use std::path::Path;
14
15#[must_use]
21pub fn generate_schema() -> Value {
22 json!({
23 "$schema": "https://json-schema.org/draft-07/schema#",
24 "title": "SsgConfig",
25 "description": "Configuration for the Static Site Generator (SSG).",
26 "type": "object",
27 "properties": {
28 "site_name": {
29 "type": "string",
30 "description": "Name of the site.",
31 "default": "MySsgSite"
32 },
33 "content_dir": {
34 "type": "string",
35 "description": "Directory containing content files.",
36 "default": "content"
37 },
38 "output_dir": {
39 "type": "string",
40 "description": "Directory for generated output files.",
41 "default": "public"
42 },
43 "template_dir": {
44 "type": "string",
45 "description": "Directory containing template files.",
46 "default": "templates"
47 },
48 "serve_dir": {
49 "type": ["string", "null"],
50 "description": "Optional directory for development server files.",
51 "default": null
52 },
53 "base_url": {
54 "type": "string",
55 "description": "Base URL of the site.",
56 "default": "http://127.0.0.1:8000",
57 "format": "uri"
58 },
59 "site_title": {
60 "type": "string",
61 "description": "Title of the site.",
62 "default": "My SSG Site"
63 },
64 "site_description": {
65 "type": "string",
66 "description": "Description of the site.",
67 "default": "A site built with SSG"
68 },
69 "language": {
70 "type": "string",
71 "description": "Language code for the site (e.g. en-GB).",
72 "default": "en-GB",
73 "pattern": "^[a-z]{2}-[A-Z]{2}$"
74 }
75 },
76 "required": [
77 "site_name",
78 "content_dir",
79 "output_dir",
80 "template_dir",
81 "base_url",
82 "site_title",
83 "site_description",
84 "language"
85 ],
86 "additionalProperties": false
87 })
88}
89
90pub fn write_schema(path: &Path) -> io::Result<()> {
104 let schema = generate_schema();
105 #[allow(clippy::expect_used)]
109 let content = serde_json::to_string_pretty(&schema)
110 .expect("hand-authored Schema is always serializable");
111 fs::write(path, content)
112}
113
114#[cfg(test)]
115#[allow(clippy::unwrap_used, clippy::expect_used)]
116mod tests {
117 use super::*;
118 use std::path::PathBuf;
119 use tempfile::tempdir;
120
121 #[test]
122 fn schema_has_correct_title() {
123 let schema = generate_schema();
124 assert_eq!(schema["title"], "SsgConfig");
125 }
126
127 #[test]
128 fn schema_has_all_required_fields() {
129 let schema = generate_schema();
130 let required = schema["required"]
131 .as_array()
132 .expect("required should be an array");
133 let names: Vec<&str> =
134 required.iter().map(|v| v.as_str().unwrap()).collect();
135 assert!(names.contains(&"site_name"));
136 assert!(names.contains(&"content_dir"));
137 assert!(names.contains(&"output_dir"));
138 assert!(names.contains(&"template_dir"));
139 assert!(names.contains(&"base_url"));
140 assert!(names.contains(&"site_title"));
141 assert!(names.contains(&"site_description"));
142 assert!(names.contains(&"language"));
143 }
144
145 #[test]
146 fn schema_properties_have_types() {
147 let schema = generate_schema();
148 let props = schema["properties"]
149 .as_object()
150 .expect("properties should be an object");
151 for (key, value) in props {
152 assert!(
153 value.get("type").is_some(),
154 "property '{key}' is missing a type"
155 );
156 }
157 }
158
159 #[test]
160 fn schema_defaults_match_config() {
161 let schema = generate_schema();
162 let props = &schema["properties"];
163 assert_eq!(props["site_name"]["default"], "MySsgSite");
164 assert_eq!(props["content_dir"]["default"], "content");
165 assert_eq!(props["output_dir"]["default"], "public");
166 assert_eq!(props["template_dir"]["default"], "templates");
167 assert_eq!(props["base_url"]["default"], "http://127.0.0.1:8000");
168 assert_eq!(props["site_title"]["default"], "My SSG Site");
169 assert_eq!(
170 props["site_description"]["default"],
171 "A site built with SSG"
172 );
173 assert_eq!(props["language"]["default"], "en-GB");
174 }
175
176 #[test]
177 fn schema_language_has_pattern() {
178 let schema = generate_schema();
179 let pattern = schema["properties"]["language"]["pattern"]
180 .as_str()
181 .expect("language should have a pattern");
182 assert_eq!(pattern, "^[a-z]{2}-[A-Z]{2}$");
183 }
184
185 #[test]
186 fn serve_dir_allows_null() {
187 let schema = generate_schema();
188 let types = schema["properties"]["serve_dir"]["type"]
189 .as_array()
190 .expect("serve_dir type should be an array");
191 let type_strs: Vec<&str> =
192 types.iter().map(|v| v.as_str().unwrap()).collect();
193 assert!(type_strs.contains(&"null"));
194 assert!(type_strs.contains(&"string"));
195 }
196
197 #[test]
198 fn write_schema_creates_valid_json_file() {
199 let dir = tempdir().expect("failed to create temp dir");
200 let path = dir.path().join("schema.json");
201 write_schema(&path).expect("write_schema failed");
202
203 let content =
204 fs::read_to_string(&path).expect("failed to read schema file");
205 let parsed: Value =
206 serde_json::from_str(&content).expect("output is not valid JSON");
207 assert_eq!(parsed["title"], "SsgConfig");
208 }
209
210 #[test]
211 fn write_schema_fails_on_bad_path() {
212 let path = PathBuf::from("/nonexistent/dir/schema.json");
213 assert!(write_schema(&path).is_err());
214 }
215}