use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
fn docs_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../docs")
.canonicalize()
.expect("docs/ directory must exist next to the workspace root")
}
const PUBLIC_API: &[&str] = &[
"de",
"directory_validator",
"group",
"report",
"ser",
"BorrowedElement",
"BorrowedSegment",
"Element",
"OwnedElement",
"OwnedSegment",
"Segment",
"Span",
"EdifactError",
"IoError",
"FunctionalGroupEnvelope",
"GroupIdentifier",
"InterchangeEnvelope",
"LenientResult",
"MessageEnvelope",
"MessageIdentifier",
"ValidatedInterchange",
"parse_ung",
"parse_unh",
"validate_envelope",
"validate_envelope_from_owned",
"validate_envelope_lenient",
"validate_envelope_lenient_from_owned",
"validate_envelope_owned",
"validate_envelope_lenient_owned",
"GroupDef",
"SegmentGroupIndexed",
"group_owned_segments_indexed",
"group_segments_indexed",
"OwnedSegmentStream",
"Parser",
"ReaderConfig",
"ServiceStringAdvice",
"Token",
"Tokenizer",
"from_bufread",
"from_bufread_stream",
"from_bufread_stream_with_config",
"from_bytes",
"from_bytes_owned",
"from_bytes_owned_with_config",
"from_bytes_windows",
"from_bytes_with_config",
"from_reader",
"from_reader_collect",
"from_reader_with_config",
"AsDataElement",
"DataElement",
"EdifactEvent",
"EventEmitter",
"MessageWriter",
"OwnedEdifactEvent",
"VecEmitter",
"Writer",
"WriterEmitter",
"elements",
"emit_sparse_segment",
"segments_to_bytes",
"segments_to_bytes_owned",
"to_writer",
"EnvelopeValidator",
"ProfileRule",
"ProfileRulePack",
"ValidationContext",
"ValidationContextBuilder",
"ValidationIssue",
"ValidationLayer",
"ValidationReport",
"ValidationRuleContext",
"ValidationSeverity",
"Validator",
"validate_each",
"ComponentRef",
"DirectoryValidator",
"DirectoryValidatorBuilder",
"ElementPath",
"ElementRef",
"OwnedComponentRef",
"OwnedElementRef",
"OwnedSegmentDef",
"SegmentDefinition",
"SegmentLayout",
"Status",
"CompositeElement",
"DecimalFloat",
"DecimalFloatDisplay",
"DispatchedMessage",
"EdifactCompositeDeserialize",
"EdifactCompositeSerialize",
"EdifactDeserialize",
"EdifactSegmentTag",
"EdifactSerialize",
"MessageDispatch",
"MessageWindow",
"MessageWindowsIter",
"MessageWindowsSliceIter",
"OwnedMessageWindow",
"SegmentAccessor",
"composite_element",
"contiguous_groups_by_qualifier",
"contiguous_groups_iter",
"deserialize",
"deserialize_all_from_reader",
"deserialize_all_streaming",
"deserialize_first_from_reader",
"deserialize_first_streaming",
"deserialize_messages_bytes",
"deserialize_messages_from_reader",
"deserialize_str",
"element_str",
"find_qualified_segment",
"find_qualified_segment_owned",
"find_segment",
"find_segment_owned",
"find_segment_typed",
"find_segments_iter",
"find_segments_typed",
"get_components_iter",
"groups_are_contiguous_by_qualifier",
"message_windows_from_reader",
"optional_component",
"optional_element",
"qualifier_matches_pattern",
"required_component",
"required_element",
"to_bytes",
"to_edifact_string",
];
fn imported_names(source: &str) -> BTreeSet<String> {
let normalised: String = source
.lines()
.map(|raw| {
let t = raw.trim_start();
t.strip_prefix("# ")
.unwrap_or_else(|| t.strip_prefix('#').unwrap_or(t))
})
.collect::<Vec<_>>()
.join("\n");
let mut found = BTreeSet::new();
let mut rest = normalised.as_str();
while let Some(pos) = rest.find("use edifact_rs::") {
rest = &rest[pos + "use edifact_rs::".len()..];
let Some(end) = rest.find(';') else { break };
let stmt = rest[..end].trim();
rest = &rest[end + 1..];
let body = stmt
.strip_prefix('{')
.and_then(|s| s.strip_suffix('}'))
.unwrap_or(stmt);
for part in body.split(',') {
let name = part
.split_whitespace()
.next()
.unwrap_or("")
.trim_end_matches('}');
if name.is_empty() || name == "self" || name == "*" {
continue;
}
let head = name.split("::").next().unwrap_or(name);
if head.chars().all(|c| c.is_alphanumeric() || c == '_') && !head.is_empty() {
found.insert(head.to_owned());
}
}
}
found
}
#[test]
fn guides_only_reference_public_api_items() {
let api: BTreeSet<&str> = PUBLIC_API.iter().copied().collect();
let mut problems: Vec<String> = Vec::new();
let mut sources: Vec<(String, String)> = Vec::new();
for entry in std::fs::read_dir(docs_dir()).expect("read docs/") {
let path = entry.expect("dir entry").path();
if path.extension().is_some_and(|e| e == "md") {
let text = std::fs::read_to_string(&path).expect("read guide");
sources.push((path.display().to_string(), text));
}
}
let readme = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../README.md");
if let Ok(text) = std::fs::read_to_string(&readme) {
sources.push((readme.display().to_string(), text));
}
assert!(!sources.is_empty(), "no guides found to check");
for (name, text) in &sources {
for item in imported_names(text) {
if !api.contains(item.as_str()) {
problems.push(format!("{name}: `edifact_rs::{item}` is not a public item"));
}
}
}
assert!(
problems.is_empty(),
"guides reference items that are not part of the public API:\n {}",
problems.join("\n ")
);
}
#[test]
fn public_api_list_is_accurate() {
#[allow(unused_imports)]
use edifact_rs::{
AsDataElement, ComponentRef, DataElement, DirectoryValidator, EdifactError, Element,
ElementPath, EventEmitter, LenientResult, OwnedComponentRef, OwnedSegment,
OwnedSegmentStream, ProfileRulePack, ReaderConfig, Segment, SegmentLayout,
ServiceStringAdvice, Span, Token, Tokenizer, ValidationContext, ValidationIssue,
ValidationReport, Writer, WriterEmitter, emit_sparse_segment, from_bytes,
to_edifact_string, validate_envelope, validate_envelope_lenient,
};
}
#[test]
fn error_reference_documents_every_stable_code() {
use edifact_rs::EdifactError;
let all = [
EdifactError::UnexpectedEof { offset: 0 },
EdifactError::InvalidDelimiter { byte: 0, offset: 0 },
EdifactError::InvalidText { offset: 0 },
EdifactError::InvalidReleaseSequence { offset: 0 },
EdifactError::MessageCountMismatch {
expected: 0,
actual: 0,
},
EdifactError::SegmentCountMismatch {
expected: 0,
actual: 0,
message_ref: String::new(),
},
EdifactError::InvalidSegmentTag(String::new()),
EdifactError::InvalidUna,
EdifactError::MissingRequiredElement {
tag: String::new(),
element_index: 0,
},
EdifactError::MissingRequiredComponent {
tag: String::new(),
element_index: 0,
component_index: 0,
},
EdifactError::InvalidUtf8,
EdifactError::InvalidSegmentForMessage {
tag: String::new(),
message_type: String::new(),
span: edifact_rs::Span::new(0, 0),
},
EdifactError::InvalidElementCount {
tag: String::new(),
actual: 0,
min: 0,
max: 0,
span: edifact_rs::Span::new(0, 0),
},
EdifactError::InvalidComponentCount {
tag: String::new(),
element_index: 0,
actual: 0,
expected: 0,
span: edifact_rs::Span::new(0, 0),
},
EdifactError::MissingSegment {
tag: String::new(),
expected_position: String::new(),
},
EdifactError::QualifierMismatch {
tag: String::new(),
actual: String::new(),
expected: String::new(),
span: edifact_rs::Span::new(0, 0),
},
EdifactError::ConditionalRequirementNotMet {
tag: String::new(),
element_index: 0,
condition: String::new(),
span: edifact_rs::Span::new(0, 0),
},
EdifactError::SegmentTooLong {
offset: 0,
limit: 0,
},
EdifactError::UnexpectedMessageType {
message_type: String::new(),
},
EdifactError::InterchangeTooLarge { count: 0 },
EdifactError::InvalidEventSequence { message: "" },
EdifactError::InvalidElementPosition,
EdifactError::InvalidFieldValue {
tag: String::new(),
element_index: 0,
value: String::new(),
},
EdifactError::UnexpectedDataToken { offset: 0 },
EdifactError::UnrecognisedSyntaxIdentifier(String::new()),
EdifactError::DuplicateReference {
tag: String::new(),
reference: String::new(),
span: edifact_rs::Span::new(0, 0),
},
EdifactError::UnknownDataElement {
tag: String::new(),
data_element: String::new(),
},
EdifactError::AmbiguousDataElement {
tag: String::new(),
data_element: String::new(),
},
EdifactError::SegmentLayoutMismatch {
expected: String::new(),
actual: String::new(),
},
];
let guide = std::fs::read_to_string(docs_dir().join("error-reference.md"))
.expect("error-reference.md must exist");
let missing: Vec<&str> = all
.iter()
.map(EdifactError::stable_code)
.filter(|code| !guide.contains(&format!("### {code} —")))
.collect();
assert!(
missing.is_empty(),
"error-reference.md is missing sections for: {missing:?}"
);
}