637 lines
22 KiB
Rust
637 lines
22 KiB
Rust
// SPDX-License-Identifier: Prosperity-3.0.0
|
|
// Copyright Scientific Computing Studio
|
|
// Source: https://git.scient.ing/education/coursebank
|
|
|
|
//! The pedagogical vocabulary: levels, cognitive processes, error types, and
|
|
//! workflow states.
|
|
|
|
use std::fmt;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Cognitive demand, following the revised Bloom taxonomy.
|
|
///
|
|
/// Serialized as the integers 1 through 5 so YAML reads `level: 3`. The derived
|
|
/// [`Ord`] follows declaration order, which is ascending demand.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
|
#[serde(try_from = "u8", into = "u8")]
|
|
pub enum Level {
|
|
/// Retrieve a fact or definition.
|
|
Remember,
|
|
/// Construct meaning; explain, compare, classify.
|
|
Understand,
|
|
/// Carry out a procedure in a given situation.
|
|
Apply,
|
|
/// Break material apart and relate the pieces.
|
|
Analyze,
|
|
/// Judge against criteria, or assemble something new.
|
|
Create,
|
|
}
|
|
|
|
impl Level {
|
|
/// Every level in ascending order, for iteration in reports.
|
|
pub const ALL: [Level; 5] = [
|
|
Level::Remember,
|
|
Level::Understand,
|
|
Level::Apply,
|
|
Level::Analyze,
|
|
Level::Create,
|
|
];
|
|
|
|
/// The numeric code used in YAML and on printed badges.
|
|
pub fn code(self) -> u8 {
|
|
match self {
|
|
Level::Remember => 1,
|
|
Level::Understand => 2,
|
|
Level::Apply => 3,
|
|
Level::Analyze => 4,
|
|
Level::Create => 5,
|
|
}
|
|
}
|
|
|
|
/// The level for a numeric code, if it is one.
|
|
///
|
|
/// Flat storage writes `0` for an unknown level rather than an empty cell,
|
|
/// because a numeric column with holes in it is awkward in every columnar
|
|
/// format. This turns that convention back into an `Option`.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `code` - the numeric code, where anything outside 1..=5 means unknown.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// The level, or `None`.
|
|
pub fn from_code(code: u8) -> Option<Level> {
|
|
match code {
|
|
1 => Some(Level::Remember),
|
|
2 => Some(Level::Understand),
|
|
3 => Some(Level::Apply),
|
|
4 => Some(Level::Analyze),
|
|
5 => Some(Level::Create),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// The category name, e.g. `"Apply"`.
|
|
pub fn name(self) -> &'static str {
|
|
match self {
|
|
Level::Remember => "Remember",
|
|
Level::Understand => "Understand",
|
|
Level::Apply => "Apply",
|
|
Level::Analyze => "Analyze",
|
|
Level::Create => "Evaluate/Create",
|
|
}
|
|
}
|
|
|
|
/// A one-line description suitable for a student-facing report.
|
|
pub fn blurb(self) -> &'static str {
|
|
match self {
|
|
Level::Remember => "recalling terms, facts, and definitions",
|
|
Level::Understand => "explaining ideas in your own words",
|
|
Level::Apply => "using a procedure in a new situation",
|
|
Level::Analyze => "taking a situation apart and relating the pieces",
|
|
Level::Create => "judging alternatives or building something new",
|
|
}
|
|
}
|
|
|
|
/// The cognitive processes that belong to this level.
|
|
pub fn processes(self) -> &'static [CognitiveProcess] {
|
|
use CognitiveProcess as P;
|
|
match self {
|
|
Level::Remember => &[P::Recognize, P::Recall],
|
|
Level::Understand => &[
|
|
P::Interpret,
|
|
P::Exemplify,
|
|
P::Classify,
|
|
P::Summarize,
|
|
P::Infer,
|
|
P::Compare,
|
|
P::Explain,
|
|
],
|
|
Level::Apply => &[P::Execute, P::Implement],
|
|
Level::Analyze => &[P::Differentiate, P::Organize, P::Attribute],
|
|
Level::Create => &[P::Check, P::Critique, P::Generate, P::Plan, P::Produce],
|
|
}
|
|
}
|
|
|
|
/// Whether a process is consistent with this level.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `process` - the process to check.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// `true` when the pairing is coherent.
|
|
pub fn allows(self, process: CognitiveProcess) -> bool {
|
|
self.processes().contains(&process)
|
|
}
|
|
}
|
|
|
|
impl TryFrom<u8> for Level {
|
|
type Error = String;
|
|
|
|
fn try_from(v: u8) -> Result<Level, String> {
|
|
match v {
|
|
1 => Ok(Level::Remember),
|
|
2 => Ok(Level::Understand),
|
|
3 => Ok(Level::Apply),
|
|
4 => Ok(Level::Analyze),
|
|
5 => Ok(Level::Create),
|
|
other => Err(format!("level must be 1 through 5, got {other}")),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<Level> for u8 {
|
|
fn from(l: Level) -> u8 {
|
|
l.code()
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Level {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "L{}", self.code())
|
|
}
|
|
}
|
|
|
|
/// The specific cognitive process an item is designed to elicit.
|
|
///
|
|
/// Naming the process, not just the level, is what makes the level claim
|
|
/// checkable: it forces you to say which of the several things "Understand"
|
|
/// could mean you actually wrote.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum CognitiveProcess {
|
|
/// Identify a previously encountered item.
|
|
Recognize,
|
|
/// Retrieve from long-term memory unaided.
|
|
Recall,
|
|
/// Restate in another representation.
|
|
Interpret,
|
|
/// Give an instance of a category.
|
|
Exemplify,
|
|
/// Assign an instance to a category.
|
|
Classify,
|
|
/// Abstract a general theme.
|
|
Summarize,
|
|
/// Draw a logical conclusion from given information.
|
|
Infer,
|
|
/// Detect correspondences between two things.
|
|
Compare,
|
|
/// Construct a cause-and-effect account.
|
|
Explain,
|
|
/// Apply a procedure to a familiar task.
|
|
Execute,
|
|
/// Apply a procedure to an unfamiliar task.
|
|
Implement,
|
|
/// Distinguish relevant from irrelevant parts.
|
|
Differentiate,
|
|
/// Determine how elements fit a structure.
|
|
Organize,
|
|
/// Determine a point of view or intent.
|
|
Attribute,
|
|
/// Test for internal consistency.
|
|
Check,
|
|
/// Judge against external criteria.
|
|
Critique,
|
|
/// Propose alternative hypotheses.
|
|
Generate,
|
|
/// Devise a procedure.
|
|
Plan,
|
|
/// Construct a product.
|
|
Produce,
|
|
}
|
|
|
|
impl CognitiveProcess {
|
|
/// The level this process belongs to.
|
|
pub fn level(self) -> Level {
|
|
for level in Level::ALL {
|
|
if level.allows(self) {
|
|
return level;
|
|
}
|
|
}
|
|
// Unreachable: every variant appears in exactly one level's list.
|
|
Level::Remember
|
|
}
|
|
|
|
/// Every process, in level order.
|
|
pub const ALL: [CognitiveProcess; 19] = [
|
|
CognitiveProcess::Recognize,
|
|
CognitiveProcess::Recall,
|
|
CognitiveProcess::Interpret,
|
|
CognitiveProcess::Exemplify,
|
|
CognitiveProcess::Classify,
|
|
CognitiveProcess::Summarize,
|
|
CognitiveProcess::Infer,
|
|
CognitiveProcess::Compare,
|
|
CognitiveProcess::Explain,
|
|
CognitiveProcess::Execute,
|
|
CognitiveProcess::Implement,
|
|
CognitiveProcess::Differentiate,
|
|
CognitiveProcess::Organize,
|
|
CognitiveProcess::Attribute,
|
|
CognitiveProcess::Check,
|
|
CognitiveProcess::Critique,
|
|
CognitiveProcess::Generate,
|
|
CognitiveProcess::Plan,
|
|
CognitiveProcess::Produce,
|
|
];
|
|
|
|
/// The snake_case token used in YAML.
|
|
pub fn as_str(self) -> &'static str {
|
|
use CognitiveProcess as P;
|
|
match self {
|
|
P::Recognize => "recognize",
|
|
P::Recall => "recall",
|
|
P::Interpret => "interpret",
|
|
P::Exemplify => "exemplify",
|
|
P::Classify => "classify",
|
|
P::Summarize => "summarize",
|
|
P::Infer => "infer",
|
|
P::Compare => "compare",
|
|
P::Explain => "explain",
|
|
P::Execute => "execute",
|
|
P::Implement => "implement",
|
|
P::Differentiate => "differentiate",
|
|
P::Organize => "organize",
|
|
P::Attribute => "attribute",
|
|
P::Check => "check",
|
|
P::Critique => "critique",
|
|
P::Generate => "generate",
|
|
P::Plan => "plan",
|
|
P::Produce => "produce",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for CognitiveProcess {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.write_str(self.as_str())
|
|
}
|
|
}
|
|
|
|
/// The category of mistake a distractor is built to capture.
|
|
///
|
|
/// This does double duty. It disciplines authoring, because a distractor you
|
|
/// cannot name an error for is probably filler. And it makes item analysis
|
|
/// legible afterwards: a high selection rate on a `DroppedStep` option tells you
|
|
/// where in a procedure students slip, which a bare letter never would.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ErrorType {
|
|
/// Confused one remembered fact for a neighboring one.
|
|
RecallConfusion,
|
|
/// Swapped two terms that sound or look alike.
|
|
TerminologySwap,
|
|
/// Holds a specific, nameable wrong model.
|
|
Misconception,
|
|
/// Knows part of the idea but not all of it.
|
|
IncompleteUnderstanding,
|
|
/// Applied a valid rule outside its scope.
|
|
Overgeneralization,
|
|
/// True but irrelevant to the question asked.
|
|
PlausibleIrrelevant,
|
|
/// Omitted a step in a procedure.
|
|
DroppedStep,
|
|
/// Inverted the direction of a relationship.
|
|
ReversedRelationship,
|
|
/// Used a procedure that does not apply here.
|
|
WrongProcedure,
|
|
/// Right method, wrong sign or order of magnitude.
|
|
SignOrMagnitudeError,
|
|
/// Ignored a condition that interacts with the answer.
|
|
IgnoresInteractingCondition,
|
|
/// Correct as far as it goes, but not the best answer.
|
|
CorrectButIncomplete,
|
|
/// Took a heuristic shortcut that usually works.
|
|
CommonShortcut,
|
|
}
|
|
|
|
impl ErrorType {
|
|
/// Every error type.
|
|
pub const ALL: [ErrorType; 13] = [
|
|
ErrorType::RecallConfusion,
|
|
ErrorType::TerminologySwap,
|
|
ErrorType::Misconception,
|
|
ErrorType::IncompleteUnderstanding,
|
|
ErrorType::Overgeneralization,
|
|
ErrorType::PlausibleIrrelevant,
|
|
ErrorType::DroppedStep,
|
|
ErrorType::ReversedRelationship,
|
|
ErrorType::WrongProcedure,
|
|
ErrorType::SignOrMagnitudeError,
|
|
ErrorType::IgnoresInteractingCondition,
|
|
ErrorType::CorrectButIncomplete,
|
|
ErrorType::CommonShortcut,
|
|
];
|
|
|
|
/// The snake_case token used in YAML.
|
|
pub fn as_str(self) -> &'static str {
|
|
use ErrorType as E;
|
|
match self {
|
|
E::RecallConfusion => "recall_confusion",
|
|
E::TerminologySwap => "terminology_swap",
|
|
E::Misconception => "misconception",
|
|
E::IncompleteUnderstanding => "incomplete_understanding",
|
|
E::Overgeneralization => "overgeneralization",
|
|
E::PlausibleIrrelevant => "plausible_irrelevant",
|
|
E::DroppedStep => "dropped_step",
|
|
E::ReversedRelationship => "reversed_relationship",
|
|
E::WrongProcedure => "wrong_procedure",
|
|
E::SignOrMagnitudeError => "sign_or_magnitude_error",
|
|
E::IgnoresInteractingCondition => "ignores_interacting_condition",
|
|
E::CorrectButIncomplete => "correct_but_incomplete",
|
|
E::CommonShortcut => "common_shortcut",
|
|
}
|
|
}
|
|
|
|
/// A short instructor-facing gloss.
|
|
pub fn gloss(self) -> &'static str {
|
|
use ErrorType as E;
|
|
match self {
|
|
E::RecallConfusion => "confused with a neighboring fact",
|
|
E::TerminologySwap => "swapped similar terms",
|
|
E::Misconception => "specific wrong model",
|
|
E::IncompleteUnderstanding => "partial grasp of the idea",
|
|
E::Overgeneralization => "applied a rule outside its scope",
|
|
E::PlausibleIrrelevant => "true but not what was asked",
|
|
E::DroppedStep => "skipped a step",
|
|
E::ReversedRelationship => "reversed the direction",
|
|
E::WrongProcedure => "used the wrong procedure",
|
|
E::SignOrMagnitudeError => "sign or magnitude slip",
|
|
E::IgnoresInteractingCondition => "ignored an interacting condition",
|
|
E::CorrectButIncomplete => "correct but not best",
|
|
E::CommonShortcut => "took a familiar shortcut",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Where an item sits in the authoring workflow.
|
|
///
|
|
/// The state gates what the validator requires. A draft may be a bare idea; an
|
|
/// approved item must be fully sourced and designed, because approval is what
|
|
/// permits it onto a graded assessment. Retired items are kept forever so the
|
|
/// bank is an append-only record of what you have asked students.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum Status {
|
|
/// Captured but not yet worked out.
|
|
Draft,
|
|
/// Written and awaiting review.
|
|
InReview,
|
|
/// Reviewed and sent back for changes.
|
|
NeedsRevision,
|
|
/// Cleared for use on a graded assessment.
|
|
Approved,
|
|
/// Withdrawn from use but retained for the record.
|
|
Retired,
|
|
}
|
|
|
|
impl Status {
|
|
/// Whether an item in this state may appear on a graded assessment.
|
|
pub fn is_usable(self) -> bool {
|
|
matches!(self, Status::Approved)
|
|
}
|
|
|
|
/// The snake_case token used in YAML.
|
|
pub fn as_str(self) -> &'static str {
|
|
match self {
|
|
Status::Draft => "draft",
|
|
Status::InReview => "in_review",
|
|
Status::NeedsRevision => "needs_revision",
|
|
Status::Approved => "approved",
|
|
Status::Retired => "retired",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Status {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.write_str(self.as_str())
|
|
}
|
|
}
|
|
|
|
/// The response format of an item.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum Format {
|
|
/// Exactly one keyed option.
|
|
SingleBestAnswer,
|
|
/// One or more keyed options; the student must find every one.
|
|
MultipleResponse,
|
|
/// Two options, True and False.
|
|
TrueFalse,
|
|
}
|
|
|
|
impl Format {
|
|
/// The QTI question type Canvas expects for this format.
|
|
pub fn qti_type(self) -> &'static str {
|
|
match self {
|
|
Format::SingleBestAnswer => "multiple_choice_question",
|
|
Format::MultipleResponse => "multiple_answers_question",
|
|
Format::TrueFalse => "true_false_question",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// How strongly an item is expected to separate strong from weak students.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum Discrimination {
|
|
/// Most prepared students get it; it anchors rather than separates.
|
|
Low,
|
|
/// Separates somewhat.
|
|
Moderate,
|
|
/// Expected to separate sharply.
|
|
High,
|
|
}
|
|
|
|
impl Discrimination {
|
|
/// The point-biserial band this expectation implies, as `(low, high)`.
|
|
///
|
|
/// Used to check an a priori expectation against the observed statistic.
|
|
pub fn expected_band(self) -> (f64, f64) {
|
|
match self {
|
|
Discrimination::Low => (-1.0, 0.20),
|
|
Discrimination::Moderate => (0.15, 0.40),
|
|
Discrimination::High => (0.30, 1.0),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// What was done about an item after reviewing its statistics.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ReviewAction {
|
|
/// Behaved as intended; leave it alone.
|
|
Keep,
|
|
/// Rewrite before reusing.
|
|
Revise,
|
|
/// Credit a defensible distractor for this administration.
|
|
AwardPartialCredit,
|
|
/// The key was wrong; fix it and rescore.
|
|
CorrectKey,
|
|
/// Withdraw from use.
|
|
Retire,
|
|
/// Keep but watch on the next administration.
|
|
Monitor,
|
|
}
|
|
|
|
/// A machine-detected problem with an item's observed behavior.
|
|
///
|
|
/// These are written by analysis, not by hand, and they are the queue you work
|
|
/// through when deciding what to revise.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum Flag {
|
|
/// Weaker students outperformed stronger ones. Almost always a keying error
|
|
/// or a genuinely ambiguous stem.
|
|
NegativeDiscrimination,
|
|
/// Barely separates students.
|
|
LowDiscrimination,
|
|
/// A distractor correlates with total score better than the key does.
|
|
DistractorOutperformsKey,
|
|
/// Strong students chose one particular distractor at a high rate, which is
|
|
/// the signature of a second defensible reading.
|
|
KeyUnderperforms,
|
|
/// Nearly everyone answered correctly; carries little information.
|
|
TooEasy,
|
|
/// Nearly everyone answered incorrectly and it did not discriminate.
|
|
TooHard,
|
|
/// Many responses were faster than plausible reading time.
|
|
HighRapidGuess,
|
|
/// A distractor almost nobody chose; it is doing no work.
|
|
NonfunctioningDistractor,
|
|
/// Performance differed across groups after conditioning on ability.
|
|
DifFlagged,
|
|
/// Marked ambiguous by hand or inferred from partial credit awarded to a
|
|
/// distractor during grading.
|
|
Ambiguous,
|
|
/// The observed difficulty was far from the difficulty you predicted.
|
|
DesignMismatch,
|
|
}
|
|
|
|
impl Flag {
|
|
/// Every flag.
|
|
pub const ALL: [Flag; 11] = [
|
|
Flag::NegativeDiscrimination,
|
|
Flag::LowDiscrimination,
|
|
Flag::DistractorOutperformsKey,
|
|
Flag::KeyUnderperforms,
|
|
Flag::TooEasy,
|
|
Flag::TooHard,
|
|
Flag::HighRapidGuess,
|
|
Flag::NonfunctioningDistractor,
|
|
Flag::DifFlagged,
|
|
Flag::Ambiguous,
|
|
Flag::DesignMismatch,
|
|
];
|
|
|
|
/// The flag's stable code, matching its YAML spelling.
|
|
pub fn as_str(self) -> &'static str {
|
|
match self {
|
|
Flag::NegativeDiscrimination => "negative_discrimination",
|
|
Flag::LowDiscrimination => "low_discrimination",
|
|
Flag::DistractorOutperformsKey => "distractor_outperforms_key",
|
|
Flag::KeyUnderperforms => "key_underperforms",
|
|
Flag::TooEasy => "too_easy",
|
|
Flag::TooHard => "too_hard",
|
|
Flag::HighRapidGuess => "high_rapid_guess",
|
|
Flag::NonfunctioningDistractor => "nonfunctioning_distractor",
|
|
Flag::DifFlagged => "dif_flagged",
|
|
Flag::Ambiguous => "ambiguous",
|
|
Flag::DesignMismatch => "design_mismatch",
|
|
}
|
|
}
|
|
|
|
/// Whether the flag should stop an item from being reused as written.
|
|
///
|
|
/// Distinguishing blocking from advisory flags is what turns analysis into a
|
|
/// workflow: a negative discrimination is a keying bug to fix before the item
|
|
/// is ever given again, while an easy item is merely uninformative.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// `true` for flags that demand a revision.
|
|
pub fn is_blocking(self) -> bool {
|
|
matches!(
|
|
self,
|
|
Flag::NegativeDiscrimination
|
|
| Flag::DistractorOutperformsKey
|
|
| Flag::Ambiguous
|
|
| Flag::KeyUnderperforms
|
|
)
|
|
}
|
|
|
|
/// A short explanation of what the flag means and what to do about it.
|
|
pub fn advice(self) -> &'static str {
|
|
match self {
|
|
Flag::NegativeDiscrimination => {
|
|
"check the key first, then the stem for a second valid reading"
|
|
}
|
|
Flag::LowDiscrimination => "expected for anchors; investigate if the level is 3+",
|
|
Flag::DistractorOutperformsKey => "the distractor may be the better answer",
|
|
Flag::KeyUnderperforms => "strong students split; look for an ambiguity",
|
|
Flag::TooEasy => "fine as an anchor, wasteful if you meant it to discriminate",
|
|
Flag::TooHard => "check for a missing prerequisite or an unclear stem",
|
|
Flag::HighRapidGuess => "position on the form or time pressure, not the item",
|
|
Flag::NonfunctioningDistractor => "replace it with a plausible error",
|
|
Flag::DifFlagged => "inspect wording for content unrelated to the objective",
|
|
Flag::Ambiguous => "rewrite the stem to exclude the second reading",
|
|
Flag::DesignMismatch => "update your expectation or revise the item",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn levels_order_by_demand() {
|
|
assert!(Level::Remember < Level::Create);
|
|
assert_eq!(Level::Apply.code(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn every_process_belongs_to_exactly_one_level() {
|
|
let mut seen = Vec::new();
|
|
for level in Level::ALL {
|
|
for p in level.processes() {
|
|
assert!(!seen.contains(p), "{p} appears under two levels");
|
|
seen.push(*p);
|
|
assert_eq!(p.level(), level);
|
|
}
|
|
}
|
|
assert_eq!(seen.len(), 19, "all processes are assigned");
|
|
}
|
|
|
|
#[test]
|
|
fn level_process_pairing_is_checked() {
|
|
assert!(Level::Apply.allows(CognitiveProcess::Implement));
|
|
assert!(!Level::Apply.allows(CognitiveProcess::Recall));
|
|
}
|
|
|
|
#[test]
|
|
fn level_serializes_as_an_integer() {
|
|
assert_eq!(serde_json::to_string(&Level::Apply).unwrap(), "3");
|
|
assert_eq!(serde_json::from_str::<Level>("4").unwrap(), Level::Analyze);
|
|
assert!(serde_json::from_str::<Level>("6").is_err());
|
|
assert!(serde_json::from_str::<Level>("0").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn processes_use_snake_case() {
|
|
assert_eq!(
|
|
serde_json::to_string(&CognitiveProcess::Implement).unwrap(),
|
|
"\"implement\""
|
|
);
|
|
assert_eq!(
|
|
serde_json::from_str::<Status>("\"in_review\"").unwrap(),
|
|
Status::InReview
|
|
);
|
|
}
|
|
}
|