diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 36895eb..0b22ad7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,7 +3,7 @@ name: Tests on: push: branches: - - "main" + - "**" tags-ignore: - "v*" pull_request: @@ -17,3 +17,16 @@ jobs: - uses: actions/checkout@v2 - name: Setup uses: ./.github/workflows/setup + - name: Install Aquascope + # tag should match mdbook-aquascope version + run: | + git clone -b v0.3.0 https://github.com/cognitive-engineering-lab/aquascope + cd aquascope + cargo make install-mdbook + - name: Test Rust package + run: cargo test --all-features --locked + - name: Lint Rust package + run: cargo clippy --all-features --locked -- -D warnings + - name: Test JS package + run: depot test + working-directory: js \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c879800..7d68dc9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,22 +35,10 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 + - name: Setup + uses: ./.github/workflows/setup with: - toolchain: stable target: ${{ matrix.target }} - profile: minimal - override: true - - name: Install cargo-make - uses: baptiste0928/cargo-install@v2 - with: - crate: cargo-make - - name: Install Depot - run: | - curl https://raw.githubusercontent.com/cognitive-engineering-lab/depot/main/scripts/install.sh | sh - echo "PATH=$HOME/.local/bin:$PATH" >> $GITHUB_ENV - - name: Initialize TS bindings - run: cargo make init-bindings - name: Build Rust package run: cargo build -p mdbook-quiz --release --locked ${{ matrix.kind == 'full' && '--features rust-editor --features aquascope' || '' }} - name: Package artifact @@ -60,9 +48,22 @@ jobs: with: name: ${{ matrix.target }}_${{ matrix.kind }} path: target/release/mdbook-quiz_${{ matrix.target }}_${{ matrix.kind }}.tar.gz + + build-schema: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup + uses: ./.github/workflows/setup + - name: Build schema + run: cargo run --bin gen-json-schema --features json-schema > mdbook-quiz.schema.json + - uses: actions/upload-artifact@v2 + with: + name: schema + path: mdbook-quiz.schema.json publish-artifacts: - needs: build-artifacts + needs: [build-artifacts, build-schema] runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v2 @@ -74,6 +75,7 @@ jobs: x86_64-apple-darwin_bare/mdbook-quiz_x86_64-apple-darwin_bare.tar.gz aarch64-unknown-linux-gnu_bare/mdbook-quiz_aarch64-unknown-linux-gnu_bare.tar.gz aarch64-apple-darwin_bare/mdbook-quiz_aarch64-apple-darwin_bare.tar.gz + schema/mdbook-quiz.schema.json publish-crate: needs: publish-artifacts diff --git a/.github/workflows/setup/action.yaml b/.github/workflows/setup/action.yaml index e76d781..4e7a9fb 100644 --- a/.github/workflows/setup/action.yaml +++ b/.github/workflows/setup/action.yaml @@ -1,4 +1,9 @@ name: Setup + +inputs: + target: + default: x86_64-unknown-linux-gnu + runs: using: composite steps: @@ -7,6 +12,8 @@ runs: profile: minimal toolchain: stable components: clippy + target: ${{ inputs.target }} + override: true - uses: Swatinem/rust-cache@v1 - name: Install cargo-make uses: baptiste0928/cargo-install@v2 @@ -15,17 +22,6 @@ runs: - name: Install Depot run: curl https://raw.githubusercontent.com/cognitive-engineering-lab/depot/main/scripts/install.sh | sh shell: bash - # Note: we have to build before test so the build script is executed - name: Initialize TS bindings run: cargo make init-bindings - shell: bash - - name: Test Rust package - run: cargo test --locked - shell: bash - - name: Lint Rust package - run: cargo clippy --locked -- -D warnings - shell: bash - - name: Test JS package - run: depot test - working-directory: js shell: bash \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index a6f478e..41cfebe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -445,6 +445,12 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dyn-clone" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd" + [[package]] name = "either" version = "1.9.0" @@ -531,6 +537,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fluid-let" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "749cff877dc1af878a0b31a41dd221a753634401ea0ef2f87b62d3171522485a" + [[package]] name = "fnv" version = "1.0.7" @@ -1173,7 +1185,9 @@ dependencies = [ name = "mdbook-quiz-schema" version = "0.1.1" dependencies = [ + "schemars", "serde", + "serde_json", "toml", "ts-rs 7.0.0", ] @@ -1183,6 +1197,7 @@ name = "mdbook-quiz-validate" version = "0.1.1" dependencies = [ "anyhow", + "fluid-let", "markdown", "mdbook-quiz-schema", "miette", @@ -1750,6 +1765,30 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schemars" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 1.0.109", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -1788,11 +1827,22 @@ dependencies = [ "syn 2.0.29", ] +[[package]] +name = "serde_derive_internals" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "serde_json" -version = "1.0.105" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ "itoa", "ryu", diff --git a/Makefile.toml b/Makefile.toml index d1e35e3..bc9473e 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -3,7 +3,6 @@ skip_core_tasks = true default_to_workspace = false -# Precommit [tasks.precommit-cargo] script = "cargo fmt && cargo clippy" @@ -13,20 +12,10 @@ script = "cd js && depot fmt" [tasks.precommit.run_task] name = ["precommit-js", "precommit-cargo"] -[tasks.init-bindings] -script = """ -cargo test -p mdbook-quiz-schema --locked export_bindings -mkdir -p js/packages/quiz/src/bindings -cp crates/mdbook-quiz-schema/bindings/* js/packages/quiz/src/bindings -""" -# Watch [tasks.watch] -script = "cargo watch -x 'install --path crates/mdbook-quiz --debug --offline --features rust-editor --features source-map' -w src -w js/packages/quiz-embed/dist --ignore-nothing" +script = "cargo watch -x 'install --path crates/mdbook-quiz --debug --offline --features rust-editor --features source-map' -w crates -w js/packages/quiz-embed/dist --ignore-nothing" -[tasks.watch.run_task] -name = ["watch-cargo"] -parallel = true [tasks.clean] script = """ @@ -35,6 +24,14 @@ cd js && depot clean && cd .. 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 +mkdir -p js/packages/quiz/src/bindings +cp crates/mdbook-quiz-schema/bindings/* js/packages/quiz/src/bindings +""" + [tasks.install] dependencies = ["init-bindings"] script = """ diff --git a/crates/mdbook-quiz-schema/Cargo.toml b/crates/mdbook-quiz-schema/Cargo.toml index db69355..015dcef 100644 --- a/crates/mdbook-quiz-schema/Cargo.toml +++ b/crates/mdbook-quiz-schema/Cargo.toml @@ -7,9 +7,14 @@ license = "MIT OR Apache-2.0" edition = "2021" repository = "https://github.com/cognitive-engineering-lab/mdbook-quiz" +[features] +json-schema = ["dep:schemars", "dep:serde_json"] + [dependencies] serde = {version = "1.0.188", features = ["derive"]} ts-rs = "7.0.0" +schemars = {version = "0.8.15", optional = true} +serde_json = {version = "1.0.107", optional = true} [dev-dependencies] -toml = { workspace = true } \ No newline at end of file +toml = { workspace = true } diff --git a/crates/mdbook-quiz-schema/src/bin/gen-json-schema.rs b/crates/mdbook-quiz-schema/src/bin/gen-json-schema.rs new file mode 100644 index 0000000..fd6a6ec --- /dev/null +++ b/crates/mdbook-quiz-schema/src/bin/gen-json-schema.rs @@ -0,0 +1,15 @@ +fn main() { + #[cfg(feature = "json-schema")] + { + use mdbook_quiz_schema::Quiz; + use schemars::schema_for; + + let schema = schema_for!(Quiz); + println!("{}", serde_json::to_string_pretty(&schema).unwrap()); + } + + #[cfg(not(feature = "json-schema"))] + { + panic!("Must run with --feature json-schema") + } +} diff --git a/crates/mdbook-quiz-schema/src/lib.rs b/crates/mdbook-quiz-schema/src/lib.rs index ac364c2..48cf5b7 100644 --- a/crates/mdbook-quiz-schema/src/lib.rs +++ b/crates/mdbook-quiz-schema/src/lib.rs @@ -1,49 +1,122 @@ +//! The schema for questions used in `mdbook-quiz`. Intended to be deserialized from a TOML file. +//! See [`Quiz`] as the top-level type. Here is an example of a quiz: +//! +//! ```toml +//! [[questions]] +//! id = "b230bed3-d6ba-4048-8b06-aa655d837b04" +//! type = "MultipleChoice" +//! prompt.prompt = "What is 1 + 1?" +//! prompt.distractors = ["1", "3", "**infinity**"] +//! answer.answer = ["2"] +//! context = """ +//! Consult your local mathematician for details. +//! """" +//! ``` +//! +//! Note that all Rust identifiers with multiple words (e.g. `does_compile`) use camelCase keys, +//! so should be written as `doesCompile` in the TOML. + +#![warn(missing_docs)] + use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use ts_rs::TS; +#[cfg(feature = "json-schema")] +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)] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] #[ts(export)] pub struct Quiz { + /// The questions of the quiz. pub questions: Vec, + + /// Context for multipart questions. + /// + /// Maps from a string key to a description of the question context. + #[ts(optional)] + pub multipart: Option>, } +/// A [Markdown](https://commonmark.org/help/) string. #[derive(Debug, Serialize, Deserialize, 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)] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] #[ts(export)] #[serde(tag = "type")] pub enum Question { + /// A [`ShortAnswer`] question. ShortAnswer(ShortAnswer), + /// A [`Tracing`] question. Tracing(Tracing), + /// A [`MultipleChoice`] question. MultipleChoice(MultipleChoice), } +/// Fields common to all question types. #[derive(Debug, Serialize, Deserialize, 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)] 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)] + pub multipart: Option, + + /// The contents of the prompt. Depends on the question type. pub prompt: Prompt, + + /// The contents of the answer. Depends on the question type. pub answer: Answer, + + /// Additional context that explains the correct answer. + /// + /// Only shown after the user has answered correctly or given up. #[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)] 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)] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] #[ts(export)] #[serde(rename_all = "lowercase")] pub enum ShortAnswerResponseFormat { + /// A one-sentence response, given an `` Short, + + /// A long-form response, given a `