use crate::message::{MessageResolver, Messages};
use crate::path::{Path, Pointer};
use indexmap::IndexMap;
use serde::ser::{Serialize, SerializeSeq, Serializer};
use serde_json::{Map, Value};
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::fmt;
#[derive(Clone, Debug, PartialEq)]
pub struct Issue {
inner: Box<Inner>,
}
#[derive(Clone, Debug, PartialEq)]
struct Inner {
path: Pointer,
code: Cow<'static, str>,
message_key: Cow<'static, str>,
meta: BTreeMap<String, Value>,
message: Option<String>,
}
impl Issue {
pub fn new(code: impl Into<Cow<'static, str>>) -> Self {
let code = code.into();
Self {
inner: Box::new(Inner {
path: Pointer::root(),
message_key: code.clone(),
code,
meta: BTreeMap::new(),
message: None,
}),
}
}
pub(crate) fn at_path(path: &Path<'_>, code: impl Into<Cow<'static, str>>) -> Self {
Self::new(code).at(path.to_pointer())
}
pub fn at(mut self, path: Pointer) -> Self {
self.inner.path = path;
self
}
pub fn with_message_key(mut self, key: impl Into<Cow<'static, str>>) -> Self {
self.inner.message_key = key.into();
self
}
pub fn with_meta(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
self.inner.meta.insert(key.into(), value.into());
self
}
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.inner.message = Some(message.into());
self
}
pub fn path(&self) -> &Pointer {
&self.inner.path
}
pub fn code(&self) -> &str {
&self.inner.code
}
pub fn message_key(&self) -> &str {
&self.inner.message_key
}
pub fn meta(&self) -> &BTreeMap<String, Value> {
&self.inner.meta
}
pub fn custom_message(&self) -> Option<&str> {
self.inner.message.as_deref()
}
pub fn message(&self) -> String {
self.message_with(Messages::english())
}
pub fn message_with(&self, resolver: &(impl MessageResolver + ?Sized)) -> String {
match &self.inner.message {
Some(message) => message.clone(),
None => resolver.resolve(self),
}
}
pub fn rebase(mut self, prefix: &Path<'_>) -> Self {
if !prefix.is_root() {
self.inner.path = self.inner.path.prefixed(prefix);
}
self
}
fn to_json_with(&self, resolver: &(impl MessageResolver + ?Sized)) -> Value {
let mut object = Map::new();
object.insert("path".into(), self.inner.path.to_string().into());
object.insert("code".into(), self.code().into());
object.insert("message".into(), self.message_with(resolver).into());
let meta: Map<String, Value> = self.inner.meta.clone().into_iter().collect();
object.insert("meta".into(), Value::Object(meta));
Value::Object(object)
}
}
impl fmt::Display for Issue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.inner.path.is_root() {
write!(f, "(root): {}", self.message())
} else {
write!(f, "{}: {}", self.inner.path, self.message())
}
}
}
impl std::error::Error for Issue {}
impl Serialize for Issue {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.to_json_with(Messages::english()).serialize(serializer)
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Issues(Vec<Issue>);
impl Issues {
pub fn new() -> Self {
Self(Vec::new())
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn iter(&self) -> std::slice::Iter<'_, Issue> {
self.0.iter()
}
pub fn as_slice(&self) -> &[Issue] {
&self.0
}
pub fn push(&mut self, issue: Issue) {
self.0.push(issue);
}
pub fn merge(&mut self, other: Issues) {
self.0.extend(other.0);
}
pub fn rebase(self, prefix: &Path<'_>) -> Self {
if prefix.is_root() {
return self;
}
Self(self.0.into_iter().map(|i| i.rebase(prefix)).collect())
}
pub fn flatten(&self) -> IndexMap<String, Vec<String>> {
self.flatten_with(Messages::english())
}
pub fn flatten_with(
&self,
resolver: &(impl MessageResolver + ?Sized),
) -> IndexMap<String, Vec<String>> {
let mut grouped: IndexMap<String, Vec<String>> = IndexMap::new();
for issue in &self.0 {
grouped
.entry(issue.path().to_string())
.or_default()
.push(issue.message_with(resolver));
}
grouped
}
pub fn to_json(&self) -> Value {
self.to_json_with(Messages::english())
}
pub fn to_json_with(&self, resolver: &(impl MessageResolver + ?Sized)) -> Value {
Value::Array(self.0.iter().map(|i| i.to_json_with(resolver)).collect())
}
}
impl From<Issue> for Issues {
fn from(issue: Issue) -> Self {
Self(vec![issue])
}
}
impl From<Vec<Issue>> for Issues {
fn from(issues: Vec<Issue>) -> Self {
Self(issues)
}
}
impl FromIterator<Issue> for Issues {
fn from_iter<T: IntoIterator<Item = Issue>>(iter: T) -> Self {
Self(iter.into_iter().collect())
}
}
impl Extend<Issue> for Issues {
fn extend<T: IntoIterator<Item = Issue>>(&mut self, iter: T) {
self.0.extend(iter);
}
}
impl IntoIterator for Issues {
type Item = Issue;
type IntoIter = std::vec::IntoIter<Issue>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> IntoIterator for &'a Issues {
type Item = &'a Issue;
type IntoIter = std::slice::Iter<'a, Issue>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl fmt::Display for Issues {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, issue) in self.0.iter().enumerate() {
if i > 0 {
f.write_str("\n")?;
}
issue.fmt(f)?;
}
Ok(())
}
}
impl std::error::Error for Issues {}
impl Serialize for Issues {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
for issue in &self.0 {
seq.serialize_element(issue)?;
}
seq.end()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codes;
use serde_json::json;
fn at(segment: &str) -> Pointer {
[segment].into_iter().collect()
}
#[test]
fn rebase_puts_the_prefix_before_the_issue_path() {
let issue = Issue::new(codes::REQUIRED).at(at("id"));
let user = Path::ROOT.key("user");
assert_eq!(issue.rebase(&user).path().to_string(), "/user/id");
}
#[test]
fn flatten_keeps_the_order_paths_were_first_seen() {
let issues: Issues = vec![
Issue::new(codes::REQUIRED).at(at("b")),
Issue::new(codes::BLANK).at(at("a")),
Issue::new(codes::BLANK).at(at("b")),
]
.into();
let flat = issues.flatten();
assert_eq!(flat.keys().collect::<Vec<_>>(), ["/b", "/a"]);
assert_eq!(flat["/b"], ["is required", "must not be blank"]);
}
#[test]
fn serialize_is_the_same_as_to_json() {
let issues: Issues = Issue::new(codes::TOO_SHORT)
.with_meta("min", 3)
.at(at("name"))
.into();
let expected = json!([{
"path": "/name",
"code": "too_short",
"message": "must be at least 3 characters",
"meta": {"min": 3}
}]);
assert_eq!(issues.to_json(), expected);
assert_eq!(serde_json::to_value(&issues).unwrap(), expected);
}
#[test]
fn a_custom_message_is_kept_in_every_language() {
let issue = Issue::new(codes::REQUIRED).with_message("give a name");
assert_eq!(issue.message(), "give a name");
assert_eq!(issue.message_with(Messages::japanese()), "give a name");
let issues: Issues = issue.into();
assert_eq!(
issues.to_json_with(Messages::japanese())[0]["message"],
"give a name"
);
}
#[test]
fn a_code_no_catalogue_knows_says_which_code_it_is() {
assert_eq!(
Issue::new("checksum").message(),
"validation failed: checksum"
);
}
}