feat: splitting
This commit is contained in:
+142
-9
@@ -31,6 +31,12 @@ const BASE: &str = "https://coursebank.dev/schema";
|
||||
pub enum Kind {
|
||||
/// `course.yaml`.
|
||||
Course,
|
||||
/// `references.yaml`.
|
||||
References,
|
||||
/// `lectures/*.yaml`.
|
||||
Lecture,
|
||||
/// `objectives/*.yaml`.
|
||||
Objective,
|
||||
/// `banks/*.yaml`.
|
||||
Bank,
|
||||
/// `assessments/*.yaml`.
|
||||
@@ -38,13 +44,23 @@ pub enum Kind {
|
||||
}
|
||||
|
||||
impl Kind {
|
||||
/// All three kinds.
|
||||
pub const ALL: [Kind; 3] = [Kind::Course, Kind::Bank, Kind::Assessment];
|
||||
/// Every kind.
|
||||
pub const ALL: [Kind; 6] = [
|
||||
Kind::Course,
|
||||
Kind::References,
|
||||
Kind::Lecture,
|
||||
Kind::Objective,
|
||||
Kind::Bank,
|
||||
Kind::Assessment,
|
||||
];
|
||||
|
||||
/// The file name a schema is written to.
|
||||
pub fn filename(self) -> &'static str {
|
||||
match self {
|
||||
Kind::Course => "course.schema.json",
|
||||
Kind::References => "references.schema.json",
|
||||
Kind::Lecture => "lecture.schema.json",
|
||||
Kind::Objective => "objective.schema.json",
|
||||
Kind::Bank => "bank.schema.json",
|
||||
Kind::Assessment => "assessment.schema.json",
|
||||
}
|
||||
@@ -80,12 +96,15 @@ impl Kind {
|
||||
pub fn schema(kind: Kind) -> Value {
|
||||
match kind {
|
||||
Kind::Course => course_schema(),
|
||||
Kind::References => references_schema(),
|
||||
Kind::Lecture => lecture_fragment_schema(),
|
||||
Kind::Objective => objective_fragment_schema(),
|
||||
Kind::Bank => bank_schema(),
|
||||
Kind::Assessment => assessment_schema(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes all three schemas to a directory.
|
||||
/// Writes every schema to a directory.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
@@ -328,6 +347,13 @@ fn lecture_schema() -> Value {
|
||||
"date": date("Date delivered."),
|
||||
"unit": { "type": "string", "description": "Unit id." },
|
||||
"slides_url": { "type": "string" },
|
||||
"teaches": {
|
||||
"type": "array",
|
||||
"description": "The objectives this session develops. Each named objective gains \
|
||||
this lecture in its `lectures` list when the course is loaded, \
|
||||
so the pair is declared once, here, while planning the lecture.",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"readings": {
|
||||
"type": "array",
|
||||
"description": "Readings assigned with this lecture, in the order you assign \
|
||||
@@ -434,7 +460,23 @@ fn reference_schema() -> Value {
|
||||
"volume": { "type": "string" },
|
||||
"issue": { "type": "string" },
|
||||
"pages": { "type": "string", "description": "Pages of the work, not of a reading." },
|
||||
"doi": { "type": "string", "description": "Bare DOI: 10.1038/nature12373." },
|
||||
"doi": {
|
||||
"type": "string",
|
||||
"pattern": "^(doi:|https?://(dx\\.)?doi\\.org/)?10\\.",
|
||||
"description": "Bare DOI: 10.1038/nature12373. For a manuscript this is \
|
||||
usually the only link worth storing, since a reading list \
|
||||
resolves it to doi.org."
|
||||
},
|
||||
"arxiv": { "type": "string", "description": "Bare arXiv id: 2301.00001." },
|
||||
"pmcid": {
|
||||
"type": "string",
|
||||
"description": "PubMed Central id, which hosts the full text: PMC3084216."
|
||||
},
|
||||
"pmid": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+$",
|
||||
"description": "PubMed id, which hosts a record about the work: 21471563."
|
||||
},
|
||||
"isbn": { "type": "string" },
|
||||
"url": { "type": "string", "description": "Canonical URL for the whole work." },
|
||||
"base_url": {
|
||||
@@ -568,6 +610,94 @@ fn course_schema() -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
/// The schema for `references.yaml`.
|
||||
fn references_schema() -> Value {
|
||||
json!({
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": format!("{BASE}/references.schema.json"),
|
||||
"title": "coursebank references file",
|
||||
"description": "The works the course cites, by citation key. One fragment of the \
|
||||
course file; see course.schema.json for the whole.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"schema_version": {
|
||||
"type": ["string", "number"],
|
||||
"description": format!("Format version; currently {SCHEMA_VERSION}. Declared in \
|
||||
course.yaml; fragments inherit it.")
|
||||
},
|
||||
"references": {
|
||||
"type": "object",
|
||||
"description": "Works by citation key.",
|
||||
"additionalProperties": reference_schema()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// The schema for one file under `lectures/`.
|
||||
fn lecture_fragment_schema() -> Value {
|
||||
json!({
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": format!("{BASE}/lecture.schema.json"),
|
||||
"title": "coursebank lecture file",
|
||||
"description": "One session: its readings, and the objectives it develops. A fragment \
|
||||
of the course file, merged on load.",
|
||||
"type": "object",
|
||||
"required": ["lectures"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"schema_version": {
|
||||
"type": ["string", "number"],
|
||||
"description": "Declared in course.yaml; fragments inherit it."
|
||||
},
|
||||
"lectures": {
|
||||
"type": "object",
|
||||
"description": "Keyed by lecture id, conventionally one entry per file.",
|
||||
"additionalProperties": lecture_schema()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// The schema for one file under `objectives/`.
|
||||
fn objective_fragment_schema() -> Value {
|
||||
json!({
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": format!("{BASE}/objective.schema.json"),
|
||||
"title": "coursebank objective file",
|
||||
"description": "One learning objective and the learning targets it decomposes into. A \
|
||||
fragment of the course file, merged on load.",
|
||||
"type": "object",
|
||||
"required": ["learning_objectives"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"schema_version": {
|
||||
"type": ["string", "number"],
|
||||
"description": "Declared in course.yaml; fragments inherit it."
|
||||
},
|
||||
"learning_objectives": {
|
||||
"type": "object",
|
||||
"description": "Keyed by objective id, conventionally one entry per file. Its \
|
||||
`lectures` list is derived from each lecture's `teaches`, so \
|
||||
leave it out unless you prefer to declare it here.",
|
||||
"additionalProperties": objective_schema()
|
||||
},
|
||||
"learning_targets": {
|
||||
"type": "object",
|
||||
"description": "The targets of this file's objective, by id. A target with no \
|
||||
`lectures` of its own inherits its objective's.",
|
||||
"additionalProperties": target_schema()
|
||||
},
|
||||
"stimuli": {
|
||||
"type": "object",
|
||||
"description": "Shared passages, figures, or data that several items refer to.",
|
||||
"additionalProperties": stimulus_schema()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// One option's schema.
|
||||
///
|
||||
/// Split out from [`item_schema`] rather than inlined, because `serde_json`'s
|
||||
@@ -583,9 +713,11 @@ fn option_schema() -> Value {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-H]$",
|
||||
"description": "Option letter. Identity, not print position — shuffled forms \
|
||||
relabel on the way out."
|
||||
"pattern": "^(o-[a-z0-9]+(-[a-z0-9]+)*|[A-H])$",
|
||||
"description": "Option id, unique within the item: `o-fourth-line`. An \
|
||||
identity, not a print position — shuffled forms relabel on the \
|
||||
way out. A single letter A-H is the pre-2.0 form; \
|
||||
`coursebank migrate options` renames it."
|
||||
},
|
||||
"text": text("The option as a student reads it."),
|
||||
"correct": { "type": "boolean" },
|
||||
@@ -1260,7 +1392,8 @@ mod tests {
|
||||
assert_eq!(props["options"]["maxItems"], 8);
|
||||
assert_eq!(
|
||||
props["options"]["items"]["properties"]["id"]["pattern"],
|
||||
"^[A-H]$"
|
||||
// Either form: the 2.0 name, or the letter it replaces.
|
||||
"^(o-[a-z0-9]+(-[a-z0-9]+)*|[A-H])$"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1283,7 +1416,7 @@ mod tests {
|
||||
let dir = std::env::temp_dir().join(format!("cb-schema-{}", std::process::id()));
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
let written = write_all(&dir).unwrap();
|
||||
assert_eq!(written.len(), 3);
|
||||
assert_eq!(written.len(), Kind::ALL.len());
|
||||
for path in &written {
|
||||
assert!(path.exists());
|
||||
let text = std::fs::read_to_string(path).unwrap();
|
||||
|
||||
@@ -1196,7 +1196,7 @@ mod tests {
|
||||
fn entry(yaml: &str) -> Entry {
|
||||
let item: Item = serde_yaml_ng::from_str(yaml).expect("item parses");
|
||||
Entry {
|
||||
uid: format!("b::{}", item.id),
|
||||
uid: item.id.clone(),
|
||||
bank: "b".into(),
|
||||
path: PathBuf::from("b.yaml"),
|
||||
index: 0,
|
||||
|
||||
@@ -469,7 +469,8 @@ pub fn to_record(
|
||||
items.push(Placement {
|
||||
number,
|
||||
item: uid.clone(),
|
||||
version: Some(e.item.version),
|
||||
version: None,
|
||||
stem_digest: Some(e.item.stem_digest()),
|
||||
fingerprint: Some(e.item.fingerprint()),
|
||||
points: Some(e.item.points(default_points)),
|
||||
bonus: is_bonus || e.item.bonus,
|
||||
|
||||
Reference in New Issue
Block a user