use std::{borrow::Cow, marker::PhantomData, ops::Deref};
#[cfg(feature = "annotate")]
use annotate_snippets::{
display_list::{DisplayList, FormatOptions},
snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation},
};
use lifetime::IntoStatic;
use serde::{Deserialize, Serialize};
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[non_exhaustive]
pub struct DetectedLanguage {
pub code: String,
#[cfg(feature = "unstable")]
pub confidence: Option<f64>,
pub name: String,
#[cfg(feature = "unstable")]
pub source: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct LanguageResponse {
pub code: String,
pub detected_language: DetectedLanguage,
pub name: String,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[non_exhaustive]
pub struct Context {
pub length: usize,
pub offset: usize,
pub text: String,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[non_exhaustive]
pub struct MoreContext {
pub line_number: usize,
pub line_offset: usize,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[non_exhaustive]
pub struct Replacement {
pub value: String,
}
impl From<String> for Replacement {
fn from(value: String) -> Self {
Self { value }
}
}
impl From<&str> for Replacement {
fn from(value: &str) -> Self {
value.to_string().into()
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[non_exhaustive]
pub struct Category {
pub id: String,
pub name: String,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[non_exhaustive]
pub struct Url {
pub value: String,
}
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Rule {
pub category: Category,
pub description: String,
pub id: String,
#[cfg(feature = "unstable")]
pub is_premium: Option<bool>,
pub issue_type: String,
#[cfg(feature = "unstable")]
pub source_file: Option<String>,
pub sub_id: Option<String>,
pub urls: Option<Vec<Url>>,
}
#[derive(PartialEq, Eq, Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Type {
pub type_name: String,
}
#[derive(PartialEq, Eq, Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Match {
pub context: Context,
#[cfg(feature = "unstable")]
pub context_for_sure_match: isize,
#[cfg(feature = "unstable")]
pub ignore_for_incomplete_sentence: bool,
pub length: usize,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub more_context: Option<MoreContext>,
pub offset: usize,
pub replacements: Vec<Replacement>,
pub rule: Rule,
pub sentence: String,
pub short_message: String,
#[cfg(feature = "unstable")]
#[serde(rename = "type")]
pub type_: Type,
}
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Software {
pub api_version: usize,
pub build_date: String,
pub name: String,
pub premium: bool,
#[cfg(feature = "unstable")]
pub premium_hint: Option<String>,
pub status: String,
pub version: String,
}
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Warnings {
pub incomplete_results: bool,
}
#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Response {
pub language: LanguageResponse,
pub matches: Vec<Match>,
#[cfg(feature = "unstable")]
pub sentence_ranges: Option<Vec<[usize; 2]>>,
pub software: Software,
#[cfg(feature = "unstable")]
pub warnings: Option<Warnings>,
}
impl Response {
pub fn iter_matches(&self) -> std::slice::Iter<'_, Match> {
self.matches.iter()
}
pub fn iter_matches_mut(&mut self) -> std::slice::IterMut<'_, Match> {
self.matches.iter_mut()
}
#[cfg(feature = "annotate")]
#[must_use]
pub fn annotate(&self, text: &str, origin: Option<&str>, color: bool) -> String {
if self.matches.is_empty() {
return "No errors were found in provided text".to_string();
}
let replacements: Vec<_> = self
.matches
.iter()
.map(|m| {
m.replacements.iter().fold(String::new(), |mut acc, r| {
if !acc.is_empty() {
acc.push_str(", ");
}
acc.push_str(&r.value);
acc
})
})
.collect();
let snippets = self.matches.iter().zip(replacements.iter()).map(|(m, r)| {
Snippet {
title: Some(Annotation {
label: Some(&m.message),
id: Some(&m.rule.id),
annotation_type: AnnotationType::Error,
}),
footer: vec![],
slices: vec![Slice {
source: &m.context.text,
line_start: 1 + text.chars().take(m.offset).filter(|c| *c == '\n').count(),
origin,
fold: true,
annotations: vec![
SourceAnnotation {
label: &m.rule.description,
annotation_type: AnnotationType::Error,
range: (m.context.offset, m.context.offset + m.context.length),
},
SourceAnnotation {
label: r,
annotation_type: AnnotationType::Help,
range: (m.context.offset, m.context.offset + m.context.length),
},
],
}],
opt: FormatOptions {
color,
..Default::default()
},
}
});
let mut annotation = String::new();
for snippet in snippets {
if !annotation.is_empty() {
annotation.push('\n');
}
annotation.push_str(&DisplayList::from(snippet).to_string());
}
annotation
}
#[must_use]
pub fn append(mut self, mut other: Self) -> Self {
#[cfg(feature = "unstable")]
if let Some(ref mut sr_other) = other.sentence_ranges {
match self.sentence_ranges {
Some(ref mut sr_self) => {
sr_self.append(sr_other);
},
None => {
std::mem::swap(&mut self.sentence_ranges, &mut other.sentence_ranges);
},
}
}
self.matches.append(&mut other.matches);
self
}
}
#[derive(Debug, Clone, PartialEq, IntoStatic)]
pub struct ResponseWithContext<'source> {
pub text: Cow<'source, str>,
pub response: Response,
pub text_length: usize,
}
impl Deref for ResponseWithContext<'_> {
type Target = Response;
fn deref(&self) -> &Self::Target {
&self.response
}
}
impl<'source> ResponseWithContext<'source> {
#[must_use]
pub fn new(text: Cow<'source, str>, response: Response) -> Self {
let text_length = text.chars().count();
Self {
text,
response,
text_length,
}
}
pub fn iter_matches(&'source self) -> std::slice::Iter<'source, Match> {
self.response.iter_matches()
}
pub fn iter_matches_mut(&mut self) -> std::slice::IterMut<'_, Match> {
self.response.iter_matches_mut()
}
#[must_use]
pub fn iter_match_positions(&self) -> MatchPositions<'_, '_, std::slice::Iter<'_, Match>> {
self.into()
}
#[must_use]
pub fn append(mut self, mut other: Self) -> Self {
let offset = self.text_length;
for m in other.iter_matches_mut() {
m.offset += offset;
}
#[cfg(feature = "unstable")]
if let Some(ref mut sr_other) = other.response.sentence_ranges {
match self.response.sentence_ranges {
Some(ref mut sr_self) => {
sr_self.append(sr_other);
},
None => {
std::mem::swap(
&mut self.response.sentence_ranges,
&mut other.response.sentence_ranges,
);
},
}
}
self.response.matches.append(&mut other.response.matches);
self.text.to_mut().push_str(&other.text);
self.text_length += other.text_length;
self
}
}
impl<'source> From<ResponseWithContext<'source>> for Response {
fn from(mut resp: ResponseWithContext<'source>) -> Self {
for (line_number, line_offset, m) in MatchPositions::new(&resp.text, &mut resp.response) {
m.more_context = Some(MoreContext {
line_number,
line_offset,
});
}
resp.response
}
}
#[derive(Clone, Debug)]
pub struct MatchPositions<'source, 'response, T: Iterator + 'response> {
text_chars: std::str::Chars<'source>,
matches: T,
line_number: usize,
line_offset: usize,
offset: usize,
_marker: PhantomData<&'response ()>,
}
impl<'source, 'response> MatchPositions<'source, 'response, std::slice::IterMut<'response, Match>> {
fn new(text: &'source str, response: &'response mut Response) -> Self {
MatchPositions {
_marker: Default::default(),
text_chars: text.chars(),
matches: response.iter_matches_mut(),
line_number: 1,
line_offset: 0,
offset: 0,
}
}
}
impl<'source, 'response> From<&'source ResponseWithContext<'source>>
for MatchPositions<'source, 'response, std::slice::Iter<'response, Match>>
where
'source: 'response,
{
fn from(response: &'source ResponseWithContext) -> Self {
MatchPositions {
_marker: Default::default(),
text_chars: response.text.chars(),
matches: response.iter_matches(),
line_number: 1,
line_offset: 0,
offset: 0,
}
}
}
impl<'source, 'response> From<&'source mut ResponseWithContext<'source>>
for MatchPositions<'source, 'response, std::slice::IterMut<'response, Match>>
where
'source: 'response,
{
fn from(response: &'source mut ResponseWithContext) -> Self {
MatchPositions {
_marker: Default::default(),
text_chars: response.text.chars(),
matches: response.response.iter_matches_mut(),
line_number: 1,
line_offset: 0,
offset: 0,
}
}
}
impl<'response, T: Iterator + 'response> MatchPositions<'_, 'response, T> {
pub fn set_line_number(mut self, line_number: usize) -> Self {
self.line_number = line_number;
self
}
fn update_line_number_and_offset(&mut self, m: &Match) {
let n = m.offset - self.offset;
for _ in 0..n {
match self.text_chars.next() {
Some('\n') => {
self.line_number += 1;
self.line_offset = 0;
},
None => {
panic!(
"text is shorter than expected, are you sure this text was the one used \
for the check request?"
)
},
_ => self.line_offset += 1,
}
}
self.offset = m.offset;
}
}
impl<'source, 'response> Iterator
for MatchPositions<'source, 'response, std::slice::Iter<'response, Match>>
where
'response: 'source,
{
type Item = (usize, usize, &'source Match);
fn next(&mut self) -> Option<Self::Item> {
if let Some(m) = self.matches.next() {
self.update_line_number_and_offset(m);
Some((self.line_number, self.line_offset, m))
} else {
None
}
}
}
impl<'source, 'response> Iterator
for MatchPositions<'source, 'response, std::slice::IterMut<'response, Match>>
where
'response: 'source,
{
type Item = (usize, usize, &'source mut Match);
fn next(&mut self) -> Option<Self::Item> {
if let Some(m) = self.matches.next() {
self.update_line_number_and_offset(m);
Some((self.line_number, self.line_offset, m))
} else {
None
}
}
}