use alloc::{format, vec::Vec};
use super::{LintFinding, LintSeverity};
#[derive(Debug, Clone, Copy)]
pub struct TextileLintInput<'a> {
pub durability_score: Option<f64>,
pub expected_wash_cycles: Option<u32>,
pub repair_count: Option<u32>,
pub repair_history_url: Option<&'a str>,
pub prior_use_cycles: Option<u32>,
pub reuse_condition: Option<&'a str>,
pub repair_score: Option<f64>,
pub disassembly_instructions: Option<&'a str>,
pub spare_parts_available: Option<bool>,
pub microplastic_shedding_mg_per_wash: Option<f64>,
pub fibres: &'a [&'a str],
}
const HIGH_DURABILITY_THRESHOLD: f64 = 8.0;
const LOW_WASH_CYCLES_THRESHOLD: u32 = 10;
#[must_use]
pub fn durability_wash_cycles_mismatch(input: &TextileLintInput<'_>) -> Option<LintFinding> {
let durability = input.durability_score?;
let cycles = input.expected_wash_cycles?;
if durability < HIGH_DURABILITY_THRESHOLD || cycles >= LOW_WASH_CYCLES_THRESHOLD {
return None;
}
Some(LintFinding {
code: "textile.durability_wash_cycles_mismatch",
field: "expectedWashCycles",
severity: LintSeverity::Notice,
message: format!(
"durabilityScore ({durability:.1}/10) is high but expectedWashCycles ({cycles}) is low — intended?"
),
})
}
#[must_use]
pub fn repair_count_without_history(input: &TextileLintInput<'_>) -> Option<LintFinding> {
let count = input.repair_count?;
if count == 0 || input.repair_history_url.is_some() {
return None;
}
Some(LintFinding {
code: "textile.repair_count_without_history",
field: "repairCount",
severity: LintSeverity::Notice,
message: format!("repairCount is {count} but repairHistoryUrl is absent — intended?"),
})
}
#[must_use]
pub fn prior_use_without_reuse_condition(input: &TextileLintInput<'_>) -> Option<LintFinding> {
let cycles = input.prior_use_cycles?;
if cycles == 0 || input.reuse_condition.is_some() {
return None;
}
Some(LintFinding {
code: "textile.prior_use_without_reuse_condition",
field: "priorUseCycles",
severity: LintSeverity::Notice,
message: format!("priorUseCycles is {cycles} but reuseCondition is absent — intended?"),
})
}
const HIGH_REPAIR_SCORE_THRESHOLD: f64 = 8.0;
#[must_use]
pub fn repair_score_high_without_support(input: &TextileLintInput<'_>) -> Option<LintFinding> {
let score = input.repair_score?;
if score < HIGH_REPAIR_SCORE_THRESHOLD
|| input.disassembly_instructions.is_some()
|| input.spare_parts_available == Some(true)
{
return None;
}
Some(LintFinding {
code: "textile.repair_score_high_without_support",
field: "repairScore",
severity: LintSeverity::Notice,
message: format!(
"repairScore ({score:.1}/10) is high but neither disassemblyInstructions nor \
sparePartsAvailable is declared — intended?"
),
})
}
const KNOWN_NATURAL_FIBRES: &[&str] = &[
"cotton", "wool", "silk", "linen", "hemp", "jute", "cashmere", "alpaca", "mohair", "flax",
"ramie", "angora",
];
fn is_known_natural(fibre: &str) -> bool {
let normalized = fibre.trim();
KNOWN_NATURAL_FIBRES
.iter()
.any(|f| normalized.eq_ignore_ascii_case(f))
}
#[must_use]
pub fn microplastic_shedding_without_synthetic_fibre(
input: &TextileLintInput<'_>,
) -> Option<LintFinding> {
let shedding = input.microplastic_shedding_mg_per_wash?;
if !shedding.is_finite() || shedding <= 0.0 {
return None;
}
if input.fibres.is_empty() || !input.fibres.iter().all(|f| is_known_natural(f)) {
return None;
}
Some(LintFinding {
code: "textile.microplastic_shedding_without_synthetic_fibre",
field: "microplasticSheddingMgPerWash",
severity: LintSeverity::Notice,
message: format!(
"microplasticSheddingMgPerWash ({shedding:.2} mg) is declared but every fibre in \
fibreComposition is a natural fibre — intended?"
),
})
}
#[must_use]
pub fn lint_textile(input: &TextileLintInput<'_>) -> Vec<LintFinding> {
let mut out = Vec::new();
out.extend(durability_wash_cycles_mismatch(input));
out.extend(repair_count_without_history(input));
out.extend(prior_use_without_reuse_condition(input));
out.extend(repair_score_high_without_support(input));
out.extend(microplastic_shedding_without_synthetic_fibre(input));
out
}
#[cfg(test)]
mod tests {
use super::*;
fn base_input() -> TextileLintInput<'static> {
TextileLintInput {
durability_score: Some(5.0),
expected_wash_cycles: Some(50),
repair_count: None,
repair_history_url: None,
prior_use_cycles: None,
reuse_condition: None,
repair_score: Some(5.0),
disassembly_instructions: None,
spare_parts_available: None,
microplastic_shedding_mg_per_wash: None,
fibres: &["cotton", "polyester"],
}
}
#[test]
fn durability_wash_cycles_consistent_passes() {
assert!(durability_wash_cycles_mismatch(&base_input()).is_none());
}
#[test]
fn high_durability_low_cycles_triggers() {
let mut input = base_input();
input.durability_score = Some(9.0);
input.expected_wash_cycles = Some(5);
let finding = durability_wash_cycles_mismatch(&input).unwrap();
assert_eq!(finding.code, "textile.durability_wash_cycles_mismatch");
}
#[test]
fn no_repairs_passes() {
assert!(repair_count_without_history(&base_input()).is_none());
}
#[test]
fn repairs_without_url_triggers() {
let mut input = base_input();
input.repair_count = Some(3);
let finding = repair_count_without_history(&input).unwrap();
assert_eq!(finding.code, "textile.repair_count_without_history");
}
#[test]
fn repairs_with_url_passes() {
let mut input = base_input();
input.repair_count = Some(3);
input.repair_history_url = Some("https://example.com/repairs/123");
assert!(repair_count_without_history(&input).is_none());
}
#[test]
fn new_item_passes() {
assert!(prior_use_without_reuse_condition(&base_input()).is_none());
}
#[test]
fn prior_use_without_condition_triggers() {
let mut input = base_input();
input.prior_use_cycles = Some(2);
let finding = prior_use_without_reuse_condition(&input).unwrap();
assert_eq!(finding.code, "textile.prior_use_without_reuse_condition");
}
#[test]
fn moderate_repair_score_passes() {
assert!(repair_score_high_without_support(&base_input()).is_none());
}
#[test]
fn high_repair_score_without_support_triggers() {
let mut input = base_input();
input.repair_score = Some(9.0);
let finding = repair_score_high_without_support(&input).unwrap();
assert_eq!(finding.code, "textile.repair_score_high_without_support");
}
#[test]
fn high_repair_score_with_spare_parts_passes() {
let mut input = base_input();
input.repair_score = Some(9.0);
input.spare_parts_available = Some(true);
assert!(repair_score_high_without_support(&input).is_none());
}
#[test]
fn shedding_with_synthetic_fibre_passes() {
let mut input = base_input();
input.microplastic_shedding_mg_per_wash = Some(12.0); assert!(microplastic_shedding_without_synthetic_fibre(&input).is_none());
}
#[test]
fn shedding_with_only_natural_fibres_triggers() {
let mut input = base_input();
input.fibres = &["cotton", "wool"];
input.microplastic_shedding_mg_per_wash = Some(12.0);
let finding = microplastic_shedding_without_synthetic_fibre(&input).unwrap();
assert_eq!(
finding.code,
"textile.microplastic_shedding_without_synthetic_fibre"
);
}
#[test]
fn no_shedding_declared_passes() {
let mut input = base_input();
input.fibres = &["cotton"];
assert!(microplastic_shedding_without_synthetic_fibre(&input).is_none());
}
#[test]
fn clean_input_produces_no_findings() {
assert!(lint_textile(&base_input()).is_empty());
}
}