diff --git a/Cargo.lock b/Cargo.lock index 41cfebe..6320b92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1165,7 +1165,7 @@ dependencies = [ [[package]] name = "mdbook-quiz" -version = "0.3.1" +version = "0.3.2" dependencies = [ "anyhow", "html-escape", @@ -1183,7 +1183,7 @@ dependencies = [ [[package]] name = "mdbook-quiz-schema" -version = "0.1.1" +version = "0.2.0" dependencies = [ "schemars", "serde", @@ -1194,7 +1194,7 @@ dependencies = [ [[package]] name = "mdbook-quiz-validate" -version = "0.1.1" +version = "0.2.0" dependencies = [ "anyhow", "fluid-let", diff --git a/Makefile.toml b/Makefile.toml index bc9473e..59f7f95 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -27,7 +27,7 @@ rm -rf js/packages/quiz/src/bindings crates/mdbook-quiz-schema/bindings [tasks.init-bindings] script = """ -cargo test -p mdbook-quiz-schema --locked export_bindings +cargo test -p mdbook-quiz-schema --locked export_bindings --features ts mkdir -p js/packages/quiz/src/bindings cp crates/mdbook-quiz-schema/bindings/* js/packages/quiz/src/bindings """ diff --git a/crates/mdbook-quiz-schema/Cargo.toml b/crates/mdbook-quiz-schema/Cargo.toml index 015dcef..f2e7527 100644 --- a/crates/mdbook-quiz-schema/Cargo.toml +++ b/crates/mdbook-quiz-schema/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mdbook-quiz-schema" -version = "0.1.1" +version = "0.2.0" authors = ["Will Crichton "] description = "Schema for quizzes used in mdbook-quiz" license = "MIT OR Apache-2.0" @@ -8,11 +8,15 @@ edition = "2021" repository = "https://github.com/cognitive-engineering-lab/mdbook-quiz" [features] +# for generating a JSON schema, used in conjunction with `cargo run --bin gen-json-schema` json-schema = ["dep:schemars", "dep:serde_json"] +# for generating Typescript bindings, used in conjunction with `cargo test export_bindings` +ts = ["dep:ts-rs"] + [dependencies] serde = {version = "1.0.188", features = ["derive"]} -ts-rs = "7.0.0" +ts-rs = {version = "7.0.0", optional = true} schemars = {version = "0.8.15", optional = true} serde_json = {version = "1.0.107", optional = true} diff --git a/crates/mdbook-quiz-schema/src/lib.rs b/crates/mdbook-quiz-schema/src/lib.rs index 48cf5b7..c74353c 100644 --- a/crates/mdbook-quiz-schema/src/lib.rs +++ b/crates/mdbook-quiz-schema/src/lib.rs @@ -20,6 +20,8 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; + +#[cfg(feature = "ts")] use ts_rs::TS; #[cfg(feature = "json-schema")] @@ -27,9 +29,9 @@ use schemars::JsonSchema; /// A quiz is the top-level data structure in mdbook-quiz. /// It represents a sequence of questions. -#[derive(Debug, Serialize, Deserialize, TS)] +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(TS))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] -#[ts(export)] pub struct Quiz { /// The questions of the quiz. pub questions: Vec, @@ -37,20 +39,20 @@ pub struct Quiz { /// Context for multipart questions. /// /// Maps from a string key to a description of the question context. - #[ts(optional)] + #[cfg_attr(feature = "ts", ts(optional))] pub multipart: Option>, } /// A [Markdown](https://commonmark.org/help/) string. -#[derive(Debug, Serialize, Deserialize, TS)] +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(TS))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] -#[ts(export)] pub struct Markdown(pub String); /// An individual question. One of several fixed types. -#[derive(Debug, Serialize, Deserialize, TS)] +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(TS))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] -#[ts(export)] #[serde(tag = "type")] pub enum Question { /// A [`ShortAnswer`] question. @@ -62,20 +64,20 @@ pub enum Question { } /// Fields common to all question types. -#[derive(Debug, Serialize, Deserialize, TS)] +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(TS))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] -#[ts(export)] #[serde(rename_all = "camelCase")] pub struct QuestionFields { /// A unique identifier for a given question. /// /// Used primarily for telemetry, as a stable identifer for questions. - #[ts(optional)] + #[cfg_attr(feature = "ts", ts(optional))] pub id: Option, /// If this key exists, then this question is part of a multipart group. /// The key must be contained in the [`Quiz::multipart`] map. - #[ts(optional)] + #[cfg_attr(feature = "ts", ts(optional))] pub multipart: Option, /// The contents of the prompt. Depends on the question type. @@ -87,21 +89,21 @@ pub struct QuestionFields { /// Additional context that explains the correct answer. /// /// Only shown after the user has answered correctly or given up. - #[ts(optional)] + #[cfg_attr(feature = "ts", ts(optional))] pub context: Option, /// If true, asks all users for a brief prose justification of their answer. /// /// Useful for getting a qualitative sense of why users respond a particular way. - #[ts(optional)] + #[cfg_attr(feature = "ts", ts(optional))] pub prompt_explanation: Option, } /// The kind of response format (and subsequent input method) that accompanies /// a given short answer questions. -#[derive(Debug, Serialize, Deserialize, TS)] +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(TS))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] -#[ts(export)] #[serde(rename_all = "lowercase")] pub enum ShortAnswerResponseFormat { /// A one-sentence response, given an `` @@ -115,74 +117,74 @@ pub enum ShortAnswerResponseFormat { } /// A prompt for a [`ShortAnswer`] question. -#[derive(Debug, Serialize, Deserialize, TS)] +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(TS))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] -#[ts(export)] pub struct ShortAnswerPrompt { /// The text of the prompt. pub prompt: Markdown, /// Format of the response. - #[ts(optional)] + #[cfg_attr(feature = "ts", ts(optional))] pub response: Option, } /// An answer for a [`ShortAnswer`] question. -#[derive(Debug, Serialize, Deserialize, TS)] +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(TS))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] -#[ts(export)] pub struct ShortAnswerAnswer { /// The exact string that answers the question. pub answer: String, /// Other acceptable strings answers. - #[ts(optional)] + #[cfg_attr(feature = "ts", ts(optional))] pub alternatives: Option>, } /// A question where users type in a response. -#[derive(Debug, Serialize, Deserialize, TS)] +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(TS))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] -#[ts(export)] pub struct ShortAnswer(pub QuestionFields); /// A prompt for a [`Tracing`] question. -#[derive(Debug, Serialize, Deserialize, TS)] +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(TS))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] -#[ts(export)] pub struct TracingPrompt { /// The contents of the program to trace. pub program: String, } /// An answer for a [`Tracing`] question. -#[derive(Debug, Serialize, Deserialize, TS)] +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(TS))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] -#[ts(export)] #[serde(rename_all = "camelCase")] pub struct TracingAnswer { /// True if the program should pass the compiler pub does_compile: bool, /// If doesCompile=true, then the contents of stdout after running the program - #[ts(optional)] + #[cfg_attr(feature = "ts", ts(optional))] pub stdout: Option, /// If doesCompile=false, then the line number of the code causing the error - #[ts(optional)] + #[cfg_attr(feature = "ts", ts(optional))] pub line_number: Option, } /// A question where users guess the output of a program. -#[derive(Debug, Serialize, Deserialize, TS)] +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(TS))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] -#[ts(export)] pub struct Tracing(pub QuestionFields); /// A prompt for a [`MultipleChoice`] question. -#[derive(Debug, Serialize, Deserialize, TS)] +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(TS))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] -#[ts(export)] #[serde(rename_all = "camelCase")] pub struct MultipleChoicePrompt { /// The text of the prompt. @@ -192,18 +194,18 @@ pub struct MultipleChoicePrompt { pub distractors: Vec, /// If defined, don't randomize distractors and put answer at this index. - #[ts(optional)] + #[cfg_attr(feature = "ts", ts(optional))] pub answer_index: Option, /// If defined, don't randomize distractors and sort answers by content. - #[ts(optional)] + #[cfg_attr(feature = "ts", ts(optional))] pub sort_answers: Option, } /// The type of response for a [`MultipleChoice`] question. -#[derive(Debug, Serialize, Deserialize, TS)] +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(TS))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] -#[ts(export)] #[serde(untagged)] pub enum MultipleChoiceAnswerFormat { /// There is one correct answer. @@ -214,18 +216,18 @@ pub enum MultipleChoiceAnswerFormat { } /// An answer for a [`MultipleChoice`] question. -#[derive(Debug, Serialize, Deserialize, TS)] +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(TS))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] -#[ts(export)] pub struct MultipleChoiceAnswer { /// The text of the correct answer. pub answer: MultipleChoiceAnswerFormat, } /// A question where users select among several possible answers. -#[derive(Debug, Serialize, Deserialize, TS)] +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(TS))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] -#[ts(export)] pub struct MultipleChoice(pub QuestionFields); #[cfg(test)] diff --git a/crates/mdbook-quiz-validate/Cargo.toml b/crates/mdbook-quiz-validate/Cargo.toml index 3c9f69c..37532e5 100644 --- a/crates/mdbook-quiz-validate/Cargo.toml +++ b/crates/mdbook-quiz-validate/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mdbook-quiz-validate" -version = "0.1.1" +version = "0.2.0" authors = ["Will Crichton "] description = "Input validation for quizzes used in mdbook-quiz" license = "MIT OR Apache-2.0" @@ -8,7 +8,7 @@ edition = "2021" repository = "https://github.com/cognitive-engineering-lab/mdbook-quiz" [dependencies] -mdbook-quiz-schema = { path = "../mdbook-quiz-schema", version = "0.1.1" } +mdbook-quiz-schema = { path = "../mdbook-quiz-schema", version = "0.2.0" } zspell = "0.3.3" markdown = "1.0.0-alpha.13" toml = { workspace = true } diff --git a/crates/mdbook-quiz-validate/src/impls/multiple_choice.rs b/crates/mdbook-quiz-validate/src/impls/multiple_choice.rs index bdcf5a1..40dedc6 100644 --- a/crates/mdbook-quiz-validate/src/impls/multiple_choice.rs +++ b/crates/mdbook-quiz-validate/src/impls/multiple_choice.rs @@ -12,12 +12,6 @@ impl Validate for MultipleChoicePrompt { self.prompt.validate(cx, tomlcast!(value.table["prompt"])); let distractors = tomlcast!(value.table["distractors"]); - cxensure!( - cx, - !self.distractors.is_empty(), - labels = vec![distractors.labeled_span()], - "Must be at least one distractor", - ); for (d, dv) in self.distractors.iter().zip(tomlcast!(distractors.array)) { d.validate(cx, dv); } @@ -95,15 +89,3 @@ prompt.sortAnswers = true "#; assert!(crate::harness(contents).is_err()); } - -#[test] -fn validate_mcq_empty_distractors() { - let contents = r#" -[[questions]] -type = "MultipleChoice" -prompt.prompt = "" -answer.answer = "" -prompt.distractors = [] # no distractors -"#; - assert!(crate::harness(contents).is_err()); -} diff --git a/crates/mdbook-quiz/Cargo.toml b/crates/mdbook-quiz/Cargo.toml index 9f74ebd..d3da2ac 100644 --- a/crates/mdbook-quiz/Cargo.toml +++ b/crates/mdbook-quiz/Cargo.toml @@ -3,7 +3,7 @@ name = "mdbook-quiz" authors = ["Will Crichton "] description = "Interactive quizzes for your mdBook" license = "MIT OR Apache-2.0" -version = "0.3.1" +version = "0.3.2" edition = "2021" include = ["/src", "/js"] repository = "https://github.com/cognitive-engineering-lab/mdbook-quiz" @@ -22,8 +22,8 @@ toml = { workspace = true } mdbook = "= 0.4.25" mdbook-preprocessor-utils = "0.1" mdbook-aquascope = {version = "0.3", optional = true} -mdbook-quiz-schema = {path = "../mdbook-quiz-schema", version = "0.1.1"} -mdbook-quiz-validate = {path = "../mdbook-quiz-validate", version = "0.1.1"} +mdbook-quiz-schema = {path = "../mdbook-quiz-schema", version = "0.2.0"} +mdbook-quiz-validate = {path = "../mdbook-quiz-validate", version = "0.2.0"} toml_edit = "0.20.0" uuid = {version = "1.4.1", features = ["v4"]} @@ -31,4 +31,4 @@ uuid = {version = "1.4.1", features = ["v4"]} mdbook-preprocessor-utils = { version = "0.1", features = ["testing"] } [build-dependencies] -anyhow = "1" \ No newline at end of file +anyhow = { workspace = true } \ No newline at end of file