use std::fmt;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
macro_rules! ops_codes {
($( $(#[$doc:meta])* $variant:ident => $wire:literal ),+ $(,)?) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum OpsCode {
$( $(#[$doc])* $variant, )+
}
impl OpsCode {
pub const ALL: &'static [OpsCode] = &[ $( OpsCode::$variant, )+ ];
pub const fn as_str(self) -> &'static str {
match self {
$( OpsCode::$variant => $wire, )+
}
}
pub fn lookup(wire: &str) -> Option<OpsCode> {
match wire {
$( $wire => Some(OpsCode::$variant), )+
_ => None,
}
}
}
};
}
ops_codes! {
InvalidInput => "INVALID_INPUT",
UpstreamUnmapped => "UPSTREAM_UNMAPPED",
InternalInvariant => "INTERNAL_INVARIANT",
JsonParseFailed => "JSON_PARSE_FAILED",
JsonSerializeFailed => "JSON_SERIALIZE_FAILED",
UnknownSchemaType => "UNKNOWN_SCHEMA_TYPE",
PresetNotFound => "PRESET_NOT_FOUND",
InputTooLarge => "INPUT_TOO_LARGE",
AssetPlanMismatch => "ASSET_PLAN_MISMATCH",
AssetIdentityConflict => "ASSET_IDENTITY_CONFLICT",
DecodeFailed => "DECODE_FAILED",
EncodeFailed => "ENCODE_FAILED",
EncodeSemanticLoss => "ENCODE_SEMANTIC_LOSS",
ValidationFailed => "VALIDATION_FAILED",
MdDecodeFailed => "MD_DECODE_FAILED",
StyleStoreFailed => "STYLE_STORE_FAILED",
NoFonts => "NO_FONTS",
StyleRebindFailed => "STYLE_REBIND_FAILED",
AnalysisFailed => "ANALYSIS_FAILED",
GridAddrInvalid => "GRID_ADDR_INVALID",
GridAddrProjectionFailed => "GRID_ADDR_PROJECTION_FAILED",
EmptyFieldValue => "EMPTY_FIELD_VALUE",
FieldNotFound => "FIELD_NOT_FOUND",
FieldNameAmbiguous => "FIELD_NAME_AMBIGUOUS",
FieldNotFillable => "FIELD_NOT_FILLABLE",
FillFailed => "FILL_FAILED",
NoValues => "NO_VALUES",
SectionOutOfRange => "SECTION_OUT_OF_RANGE",
SectionIndexMismatch => "SECTION_INDEX_MISMATCH",
PatchFailed => "PATCH_FAILED",
SectionWorkflowFailed => "SECTION_WORKFLOW_FAILED",
ReadTargetRequired => "READ_TARGET_REQUIRED",
ReadParasInvalid => "READ_PARAS_INVALID",
ReadParasWithoutSection => "READ_PARAS_WITHOUT_SECTION",
ReadSectionOutOfRange => "READ_SECTION_OUT_OF_RANGE",
ReadParaRangeInvalid => "READ_PARA_RANGE_INVALID",
ReadTableOutOfRange => "READ_TABLE_OUT_OF_RANGE",
ReadFieldNotFound => "READ_FIELD_NOT_FOUND",
TableNotFound => "TABLE_NOT_FOUND",
TableGridInvalid => "TABLE_GRID_INVALID",
TableGridUnaddressable => "TABLE_GRID_UNADDRESSABLE",
CellNotFound => "CELL_NOT_FOUND",
CellLabelAmbiguous => "CELL_LABEL_AMBIGUOUS",
CellHasNonTextContent => "CELL_HAS_NON_TEXT_CONTENT",
CellTargetDuplicate => "CELL_TARGET_DUPLICATE",
CellTargetConflict => "CELL_TARGET_CONFLICT",
InputNotRoundtripSafe => "INPUT_NOT_ROUNDTRIP_SAFE",
InputEntriesNotCarried => "INPUT_ENTRIES_NOT_CARRIED",
SetCellCodecFailed => "SET_CELL_CODEC_FAILED",
SetCellFailed => "SET_CELL_FAILED",
InvalidSetCellArgs => "INVALID_SET_CELL_ARGS",
InvalidSetCellMap => "INVALID_SET_CELL_MAP",
StampFailed => "STAMP_FAILED",
StampCodecFailed => "STAMP_CODEC_FAILED",
StampManifestInvariant => "STAMP_MANIFEST_INVARIANT",
StampSourceHashMismatch => "STAMP_SOURCE_HASH_MISMATCH",
StampDeltaMismatch => "STAMP_DELTA_MISMATCH",
StampCellNotAnchor => "STAMP_CELL_NOT_ANCHOR",
StampCellNotEmpty => "STAMP_CELL_NOT_EMPTY",
StampLabelDrift => "STAMP_LABEL_DRIFT",
StampCellNotCandidate => "STAMP_CELL_NOT_CANDIDATE",
StampCellTargetDuplicate => "STAMP_CELL_TARGET_DUPLICATE",
StampNameEmpty => "STAMP_NAME_EMPTY",
StampHintBlank => "STAMP_HINT_BLANK",
StampNameDuplicate => "STAMP_NAME_DUPLICATE",
StampNameCollision => "STAMP_NAME_COLLISION",
StampCandidateUncovered => "STAMP_CANDIDATE_UNCOVERED",
StampSpecStale => "STAMP_SPEC_STALE",
StampMarkerMismatch => "STAMP_MARKER_MISMATCH",
StampSpecDuplicate => "STAMP_SPEC_DUPLICATE",
InvalidStampMap => "INVALID_STAMP_MAP",
MissingSourceSha256 => "MISSING_SOURCE_SHA256",
StructuralEditFailed => "STRUCTURAL_EDIT_FAILED",
StructuralCodec => "STRUCTURAL_CODEC",
ParagraphOutOfRange => "PARAGRAPH_OUT_OF_RANGE",
DuplicateTarget => "DUPLICATE_TARGET",
ReferenceStranded => "REFERENCE_STRANDED",
HardBreakLoss => "HARD_BREAK_LOSS",
EmptySection => "EMPTY_SECTION",
SectionPropertiesParagraph => "SECTION_PROPERTIES_PARAGRAPH",
SpanCountMismatch => "SPAN_COUNT_MISMATCH",
SelfVerifyFailed => "SELF_VERIFY_FAILED",
MultiParagraphText => "MULTI_PARAGRAPH_TEXT",
InsertBeforeSectionProperties => "INSERT_BEFORE_SECTION_PROPERTIES",
DeleteNoTarget => "DELETE_NO_TARGET",
InsertTextRequired => "INSERT_TEXT_REQUIRED",
Hwp5DecodeFailed => "HWP5_DECODE_FAILED",
Hwp5ConvertFailed => "HWP5_CONVERT_FAILED",
UnrecognizedFormat => "UNRECOGNIZED_FORMAT",
PdfRenderFailed => "PDF_RENDER_FAILED",
InvalidDiscovery => "INVALID_DISCOVERY",
}
impl fmt::Display for OpsCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnknownOpsCode(pub String);
impl fmt::Display for UnknownOpsCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "unknown ops code `{}`", self.0)
}
}
impl std::error::Error for UnknownOpsCode {}
impl std::str::FromStr for OpsCode {
type Err = UnknownOpsCode;
fn from_str(wire: &str) -> Result<Self, Self::Err> {
OpsCode::lookup(wire).ok_or_else(|| UnknownOpsCode(wire.to_owned()))
}
}
impl Serialize for OpsCode {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for OpsCode {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let wire = String::deserialize(deserializer)?;
wire.parse().map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[non_exhaustive]
pub struct WarningInfo {
pub code: String,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hint: Option<String>,
}
impl WarningInfo {
pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
Self { code: code.into(), message: message.into(), hint: None }
}
pub fn coded(code: OpsCode, message: impl Into<String>) -> Self {
Self::new(code.as_str(), message)
}
#[must_use]
pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
self.hint = Some(hint.into());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn every_code_round_trips_through_its_wire_string() {
for code in OpsCode::ALL {
assert_eq!(code.as_str().parse::<OpsCode>(), Ok(*code), "{code:?}");
assert_eq!(OpsCode::lookup(code.as_str()), Some(*code));
}
}
#[test]
fn wire_strings_are_unique_screaming_snake() {
let mut seen = HashSet::new();
for code in OpsCode::ALL {
let s = code.as_str();
assert!(seen.insert(s), "duplicate wire string {s}");
assert!(
s.bytes().all(|b| b.is_ascii_uppercase() || b.is_ascii_digit() || b == b'_'),
"{s} is not SCREAMING_SNAKE"
);
assert!(!s.starts_with('_') && !s.ends_with('_'), "{s}");
}
}
#[test]
fn serde_uses_the_wire_string() {
let json = serde_json::to_string(&OpsCode::EncodeSemanticLoss).unwrap();
assert_eq!(json, "\"ENCODE_SEMANTIC_LOSS\"");
let back: OpsCode = serde_json::from_str(&json).unwrap();
assert_eq!(back, OpsCode::EncodeSemanticLoss);
assert!(serde_json::from_str::<OpsCode>("\"NOPE\"").is_err());
}
#[test]
fn unknown_wire_string_is_none() {
assert_eq!(OpsCode::lookup("decode_failed"), None);
assert_eq!("".parse::<OpsCode>(), Err(UnknownOpsCode(String::new())));
assert_eq!(UnknownOpsCode("X".into()).to_string(), "unknown ops code `X`");
}
#[test]
fn warning_info_serialises_without_null_hint() {
let w = WarningInfo::coded(OpsCode::EncodeSemanticLoss, "note head skipped");
assert_eq!(
serde_json::to_string(&w).unwrap(),
r#"{"code":"ENCODE_SEMANTIC_LOSS","message":"note head skipped"}"#
);
let w = w.with_hint("re-export the section");
let v: serde_json::Value = serde_json::to_value(&w).unwrap();
assert_eq!(v["hint"], "re-export the section");
let back: WarningInfo = serde_json::from_value(v).unwrap();
assert_eq!(back, w);
}
#[test]
fn display_is_the_wire_string() {
assert_eq!(OpsCode::PresetNotFound.to_string(), "PRESET_NOT_FOUND");
}
}