use serde_json::{Map, Value};
use crate::error::{ErrorCode, ProviderError};
pub const MAX_POINTER_LEN: usize = 128;
pub const DEFAULT_MAX_EXPANSIONS: usize = 32;
pub const MAX_STRUCTURAL_DEPTH: usize = 256;
pub const ANNOTATIONS: &[&str] = &[
"$comment",
"default",
"deprecated",
"description",
"examples",
"readOnly",
"title",
"writeOnly",
];
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum DialectError {
#[error("{pointer}: the branches of {keyword} were not provably disjoint")]
UnprovenUnion {
pointer: String,
keyword: String,
},
#[error("{pointer}: $ref carries the constraining sibling {keyword}")]
ConstrainingRefSibling {
pointer: String,
keyword: String,
},
#[error("{pointer}: $ref is not a local definition reference")]
UnresolvableRef {
pointer: String,
},
#[error("{pointer}: the definition is recursive and cannot be inlined")]
RecursiveRef {
pointer: String,
},
#[error("{pointer}: the schema goes deeper than {limit} levels")]
TooDeep {
pointer: String,
limit: usize,
},
#[error("{pointer}: expected an object sub-schema")]
NotAnObject {
pointer: String,
},
}
impl DialectError {
#[must_use]
pub fn pointer(&self) -> &str {
match self {
Self::UnprovenUnion { pointer, .. }
| Self::ConstrainingRefSibling { pointer, .. }
| Self::UnresolvableRef { pointer }
| Self::RecursiveRef { pointer }
| Self::TooDeep { pointer, .. }
| Self::NotAnObject { pointer } => pointer,
}
}
#[must_use]
pub fn keyword(&self) -> &str {
match self {
Self::UnprovenUnion { keyword, .. } | Self::ConstrainingRefSibling { keyword, .. } => {
keyword
}
Self::UnresolvableRef { .. } | Self::RecursiveRef { .. } => "$ref",
Self::TooDeep { .. } => "depth",
Self::NotAnObject { .. } => "schema",
}
}
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::UnprovenUnion { .. } => "unproven_union",
Self::ConstrainingRefSibling { .. } => "constraining_ref_sibling",
Self::UnresolvableRef { .. } => "unresolvable_ref",
Self::RecursiveRef { .. } => "recursive_ref",
Self::TooDeep { .. } => "too_deep",
Self::NotAnObject { .. } => "not_an_object",
}
}
#[must_use]
pub fn code(&self) -> ErrorCode {
ErrorCode::new(self.as_str())
}
}
impl From<DialectError> for ProviderError {
fn from(value: DialectError) -> Self {
Self::unsupported("schema_dialect").with_code(value.as_str())
}
}
fn child(pointer: &str, segment: &str) -> String {
let escaped = segment.replace('~', "~0").replace('/', "~1");
format!("{pointer}/{escaped}")
}
fn short(pointer: &str) -> String {
if pointer.is_empty() {
return String::from("/");
}
if pointer.len() <= MAX_POINTER_LEN {
return pointer.to_owned();
}
let mut end = MAX_POINTER_LEN;
while end > 0 && !pointer.is_char_boundary(end) {
end -= 1;
}
format!("{}…", &pointer[..end])
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Disjointness {
ByConstant,
ByType,
ByDiscriminant {
property: String,
},
ByDiscriminantPairs,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct TypeSet(u8);
impl TypeSet {
const NULL: u8 = 1;
const BOOLEAN: u8 = 1 << 1;
const INTEGER: u8 = 1 << 2;
const NUMBER: u8 = 1 << 3;
const STRING: u8 = 1 << 4;
const ARRAY: u8 = 1 << 5;
const OBJECT: u8 = 1 << 6;
const ALL: u8 = 0b0111_1111;
const fn all() -> Self {
Self(Self::ALL)
}
const fn empty() -> Self {
Self(0)
}
const fn is_empty(self) -> bool {
self.0 == 0
}
fn named(name: &str) -> Self {
Self(match name {
"null" => Self::NULL,
"boolean" => Self::BOOLEAN,
"integer" => Self::INTEGER,
"number" => Self::NUMBER | Self::INTEGER,
"string" => Self::STRING,
"array" => Self::ARRAY,
"object" => Self::OBJECT,
_ => 0,
})
}
fn of_value(value: &Value) -> Self {
Self(match value {
Value::Null => Self::NULL,
Value::Bool(_) => Self::BOOLEAN,
Value::Number(number) if number.is_f64() => Self::NUMBER,
Value::Number(_) => Self::INTEGER,
Value::String(_) => Self::STRING,
Value::Array(_) => Self::ARRAY,
Value::Object(_) => Self::OBJECT,
})
}
const fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
const fn intersects(self, other: Self) -> bool {
self.0 & other.0 != 0
}
}
fn type_set(schema: &Value) -> TypeSet {
let Some(object) = schema.as_object() else {
return TypeSet::all();
};
if let Some(constant) = object.get("const") {
return TypeSet::of_value(constant);
}
if let Some(Value::Array(values)) = object.get("enum") {
return values.iter().fold(TypeSet::empty(), |acc, value| {
acc.union(TypeSet::of_value(value))
});
}
match object.get("type") {
Some(Value::String(name)) => TypeSet::named(name),
Some(Value::Array(names)) => names.iter().fold(TypeSet::empty(), |acc, name| {
name.as_str()
.map_or(TypeSet::all(), |name| acc.union(TypeSet::named(name)))
}),
_ => TypeSet::all(),
}
}
fn literals(schema: &Value) -> Option<Vec<&Value>> {
let object = schema.as_object()?;
if let Some(constant) = object.get("const") {
return Some(vec![constant]);
}
match object.get("enum") {
Some(Value::Array(values)) if !values.is_empty() => Some(values.iter().collect()),
_ => None,
}
}
fn literals_disjoint(left: &[&Value], right: &[&Value]) -> bool {
!left.iter().any(|one| right.contains(one))
}
fn required_names(schema: &Value) -> Vec<&str> {
schema
.get("required")
.and_then(Value::as_array)
.map(|names| names.iter().filter_map(Value::as_str).collect())
.unwrap_or_default()
}
#[must_use]
pub fn prove_disjoint(branches: &[Value]) -> Option<Disjointness> {
if branches.len() < 2 {
return Some(Disjointness::ByConstant);
}
let pinned: Option<Vec<Vec<&Value>>> = branches.iter().map(literals).collect();
if let Some(sets) = pinned
&& pairwise(&sets, |left, right| literals_disjoint(left, right))
{
return Some(Disjointness::ByConstant);
}
let types: Vec<TypeSet> = branches.iter().map(type_set).collect();
if !types.iter().any(|set| set.is_empty())
&& pairwise(&types, |left, right| !left.intersects(*right))
{
return Some(Disjointness::ByType);
}
for candidate in required_names(&branches[0]) {
let pinned: Option<Vec<Vec<&Value>>> = branches
.iter()
.map(|branch| pinned_property(branch, candidate))
.collect();
if let Some(sets) = pinned
&& pairwise(&sets, |left, right| literals_disjoint(left, right))
{
return Some(Disjointness::ByDiscriminant {
property: candidate.to_owned(),
});
}
}
let candidates: Vec<&str> = required_names(&branches[0]);
if !candidates.is_empty()
&& pairwise(branches, |left, right| {
candidates.iter().any(|candidate| {
match (
pinned_property(left, candidate),
pinned_property(right, candidate),
) {
(Some(one), Some(other)) => literals_disjoint(&one, &other),
_ => false,
}
})
})
{
return Some(Disjointness::ByDiscriminantPairs);
}
None
}
fn pinned_property<'a>(branch: &'a Value, property: &str) -> Option<Vec<&'a Value>> {
if !required_names(branch).contains(&property) {
return None;
}
branch.get("properties")?.get(property).and_then(literals)
}
fn pairwise<T>(items: &[T], holds: impl Fn(&T, &T) -> bool) -> bool {
items
.iter()
.enumerate()
.all(|(index, left)| items[index + 1..].iter().all(|right| holds(left, right)))
}
pub fn narrow_unions(schema: &Value) -> Result<Value, DialectError> {
fn walk(node: &Value, pointer: &str) -> Result<Value, DialectError> {
match node {
Value::Object(map) => {
let mut out = Map::with_capacity(map.len());
for (key, value) in map {
let child = child(pointer, key);
if key == "oneOf" {
let branches =
value.as_array().ok_or_else(|| DialectError::NotAnObject {
pointer: short(&child),
})?;
if prove_disjoint(branches).is_none() {
return Err(DialectError::UnprovenUnion {
pointer: short(pointer),
keyword: String::from("oneOf"),
});
}
out.insert(String::from("anyOf"), walk(value, &child)?);
} else {
out.insert(key.clone(), walk(value, &child)?);
}
}
Ok(Value::Object(out))
}
Value::Array(items) => items
.iter()
.enumerate()
.map(|(index, item)| walk(item, &child(pointer, &index.to_string())))
.collect::<Result<Vec<_>, _>>()
.map(Value::Array),
other => Ok(other.clone()),
}
}
walk(schema, "")
}
pub fn lift_ref_siblings(schema: &Value) -> Result<Value, DialectError> {
fn walk(node: &Value, pointer: &str) -> Result<Value, DialectError> {
match node {
Value::Object(map) if map.contains_key("$ref") && map.len() > 1 => {
let mut out = Map::with_capacity(map.len());
for key in map.keys().filter(|key| key.as_str() != "$ref") {
if !ANNOTATIONS.contains(&key.as_str()) {
return Err(DialectError::ConstrainingRefSibling {
pointer: short(pointer),
keyword: key.clone(),
});
}
}
for (key, value) in map {
if key == "$ref" {
continue;
}
out.insert(key.clone(), value.clone());
}
out.insert(
String::from("anyOf"),
Value::Array(vec![serde_json::json!({"$ref": map["$ref"].clone()})]),
);
Ok(Value::Object(out))
}
Value::Object(map) => map
.iter()
.map(|(key, value)| {
walk(value, &child(pointer, key)).map(|value| (key.clone(), value))
})
.collect::<Result<Map<_, _>, _>>()
.map(Value::Object),
Value::Array(items) => items
.iter()
.enumerate()
.map(|(index, item)| walk(item, &child(pointer, &index.to_string())))
.collect::<Result<Vec<_>, _>>()
.map(Value::Array),
other => Ok(other.clone()),
}
}
walk(schema, "")
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Closure {
pub schema: Value,
pub forced_required: Vec<String>,
}
#[must_use]
pub fn close_objects(schema: &Value) -> Closure {
fn walk(node: &Value, pointer: &str, forced: &mut Vec<String>) -> Value {
match node {
Value::Object(map) => {
let mut out = Map::with_capacity(map.len() + 2);
for (key, value) in map {
out.insert(key.clone(), walk(value, &child(pointer, key), forced));
}
if let Some(Value::Object(properties)) = map.get("properties") {
let already: Vec<&str> = required_names(node);
for name in properties.keys() {
if !already.contains(&name.as_str()) {
forced.push(short(&child(&child(pointer, "properties"), name)));
}
}
out.insert(
String::from("required"),
Value::Array(
properties
.keys()
.map(|name| Value::String(name.clone()))
.collect(),
),
);
out.insert(String::from("additionalProperties"), Value::Bool(false));
}
Value::Object(out)
}
Value::Array(items) => Value::Array(
items
.iter()
.enumerate()
.map(|(index, item)| walk(item, &child(pointer, &index.to_string()), forced))
.collect(),
),
other => other.clone(),
}
}
let mut forced_required = Vec::new();
let schema = walk(schema, "", &mut forced_required);
forced_required.sort();
Closure {
schema,
forced_required,
}
}
pub fn inline_definitions(schema: &Value, max_expansions: usize) -> Result<Value, DialectError> {
const TABLES: [&str; 2] = ["$defs", "definitions"];
struct Inliner<'a> {
tables: Vec<&'a Map<String, Value>>,
max_expansions: usize,
}
impl Inliner<'_> {
fn resolve(&self, reference: &str) -> Option<&Value> {
let rest = reference.strip_prefix("#/")?;
let (table, name) = rest.split_once('/')?;
if !TABLES.contains(&table) || name.contains('/') {
return None;
}
self.tables.iter().find_map(|found| found.get(name))
}
fn walk(
&self,
node: &Value,
pointer: &str,
depth: usize,
open: &mut Vec<String>,
) -> Result<Value, DialectError> {
if open.len() > self.max_expansions {
return Err(DialectError::TooDeep {
pointer: short(pointer),
limit: self.max_expansions,
});
}
if depth > MAX_STRUCTURAL_DEPTH {
return Err(DialectError::TooDeep {
pointer: short(pointer),
limit: MAX_STRUCTURAL_DEPTH,
});
}
match node {
Value::Object(map) if map.contains_key("$ref") => {
let reference =
map["$ref"]
.as_str()
.ok_or_else(|| DialectError::UnresolvableRef {
pointer: short(pointer),
})?;
if open.iter().any(|seen| seen == reference) {
return Err(DialectError::RecursiveRef {
pointer: short(pointer),
});
}
let target =
self.resolve(reference)
.ok_or_else(|| DialectError::UnresolvableRef {
pointer: short(pointer),
})?;
for key in map.keys() {
if key == "$ref" || TABLES.contains(&key.as_str()) {
continue;
}
if !ANNOTATIONS.contains(&key.as_str()) {
return Err(DialectError::ConstrainingRefSibling {
pointer: short(pointer),
keyword: key.clone(),
});
}
}
open.push(reference.to_owned());
let expanded = self.walk(target, pointer, depth + 1, open);
open.pop();
let mut expanded = match expanded? {
Value::Object(body) => body,
other => return Ok(other),
};
for (key, value) in map {
if key != "$ref" && !TABLES.contains(&key.as_str()) {
expanded.insert(key.clone(), value.clone());
}
}
Ok(Value::Object(expanded))
}
Value::Object(map) => map
.iter()
.filter(|(key, _)| !TABLES.contains(&key.as_str()))
.map(|(key, value)| {
self.walk(value, &child(pointer, key), depth + 1, open)
.map(|value| (key.clone(), value))
})
.collect::<Result<Map<_, _>, _>>()
.map(Value::Object),
Value::Array(items) => items
.iter()
.enumerate()
.map(|(index, item)| {
self.walk(item, &child(pointer, &index.to_string()), depth + 1, open)
})
.collect::<Result<Vec<_>, _>>()
.map(Value::Array),
other => Ok(other.clone()),
}
}
}
let tables = TABLES
.iter()
.filter_map(|name| schema.get(*name).and_then(Value::as_object))
.collect();
let inliner = Inliner {
tables,
max_expansions,
};
inliner.walk(schema, "", 0, &mut Vec::new())
}
#[must_use]
pub fn collapse_literal_union(branches: &[Value]) -> Option<(Vec<Value>, String)> {
let mut values = Vec::with_capacity(branches.len());
let mut lines = Vec::new();
for branch in branches {
let pinned = literals(branch)?;
if pinned.len() != 1 {
return None;
}
let value = pinned[0];
if let (Some(name), Some(doc)) = (
value.as_str(),
branch.get("description").and_then(Value::as_str),
) {
lines.push(format!("«{name}»: {doc}"));
}
values.push(value.clone());
}
Some((values, lines.join("\n")))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn a_tagged_enum_is_disjoint_by_its_tag() {
let branches = [
json!({"type": "object", "required": ["kind", "value"],
"properties": {"kind": {"const": "token"}, "value": {"type": "string"}}}),
json!({"type": "object", "required": ["kind"],
"properties": {"kind": {"const": "new_case"}}}),
];
assert_eq!(
prove_disjoint(&branches),
Some(Disjointness::ByDiscriminant {
property: "kind".to_owned()
})
);
}
#[test]
fn branches_sharing_a_tag_are_separated_by_a_second_field() {
let branches = [
json!({"type": "object", "required": ["kind", "operation"],
"properties": {"kind": {"const": "apply"},
"operation": {"enum": ["a.set", "a.clear"]}}}),
json!({"type": "object", "required": ["kind", "operation"],
"properties": {"kind": {"const": "apply"},
"operation": {"enum": ["a.create"]}}}),
json!({"type": "object", "required": ["kind"],
"properties": {"kind": {"const": "start"}}}),
];
assert_eq!(
prove_disjoint(&branches),
Some(Disjointness::ByDiscriminantPairs)
);
}
#[test]
fn a_shared_tag_with_an_overlapping_second_field_is_not_disjoint() {
let branches = [
json!({"type": "object", "required": ["kind", "operation"],
"properties": {"kind": {"const": "apply"},
"operation": {"enum": ["a.set", "a.clear"]}}}),
json!({"type": "object", "required": ["kind", "operation"],
"properties": {"kind": {"const": "apply"},
"operation": {"enum": ["a.clear"]}}}),
];
assert_eq!(prove_disjoint(&branches), None);
}
#[test]
fn a_repeated_tag_is_not_disjoint() {
let branches = [
json!({"type": "object", "required": ["kind"],
"properties": {"kind": {"const": "token"}}}),
json!({"type": "object", "required": ["kind"],
"properties": {"kind": {"const": "token"}}}),
];
assert_eq!(prove_disjoint(&branches), None);
}
#[test]
fn a_tag_that_one_branch_leaves_optional_is_not_disjoint() {
let branches = [
json!({"type": "object", "required": ["kind"],
"properties": {"kind": {"const": "a"}}}),
json!({"type": "object", "properties": {"kind": {"const": "b"}}}),
];
assert_eq!(prove_disjoint(&branches), None);
}
#[test]
fn distinct_constants_are_disjoint_and_shared_ones_are_not() {
assert_eq!(
prove_disjoint(&[json!({"const": "a"}), json!({"const": "b"})]),
Some(Disjointness::ByConstant)
);
assert_eq!(
prove_disjoint(&[json!({"enum": ["a", "b"]}), json!({"enum": ["b", "c"]})]),
None
);
}
#[test]
fn distinct_types_are_disjoint_but_number_contains_integer() {
assert_eq!(
prove_disjoint(&[json!({"type": "string"}), json!({"type": "object"})]),
Some(Disjointness::ByType)
);
assert_eq!(
prove_disjoint(&[json!({"type": "number"}), json!({"type": "integer"})]),
None
);
}
#[test]
fn an_unconstrained_branch_defeats_every_proof() {
assert_eq!(
prove_disjoint(&[json!({"type": "string"}), json!({})]),
None
);
}
#[test]
fn narrowing_rewrites_what_it_proves_and_refuses_what_it_cannot() {
let proven = narrow_unions(&json!({
"properties": {"t": {"oneOf": [{"const": "a"}, {"const": "b"}]}}
}))
.expect("provably disjoint");
assert_eq!(
proven["properties"]["t"]["anyOf"],
json!([{"const": "a"}, {"const": "b"}])
);
let error = narrow_unions(&json!({
"properties": {"t": {"oneOf": [{"type": "object"}, {"type": "object"}]}}
}))
.expect_err("two open objects overlap");
assert_eq!(error.keyword(), "oneOf");
assert_eq!(error.pointer(), "/properties/t");
assert_eq!(error.as_str(), "unproven_union");
}
#[test]
fn a_ref_keeps_its_description_and_refuses_a_constraint() {
let lifted = lift_ref_siblings(&json!({
"properties": {"a": {"$ref": "#/$defs/X", "description": "the a"}}
}))
.expect("an annotation lifts");
assert_eq!(
lifted["properties"]["a"],
json!({"description": "the a", "anyOf": [{"$ref": "#/$defs/X"}]})
);
let error = lift_ref_siblings(&json!({
"properties": {"a": {"$ref": "#/$defs/X", "minLength": 3}}
}))
.expect_err("a constraint cannot be lifted");
assert_eq!(error.keyword(), "minLength");
assert_eq!(error.as_str(), "constraining_ref_sibling");
}
#[test]
fn closing_forces_every_property_and_names_what_it_forced() {
let closed = close_objects(&json!({
"type": "object",
"required": ["a"],
"properties": {
"a": {"type": "string"},
"b": {"type": ["string", "null"]},
"c": {"type": "object", "properties": {"d": {"type": "string"}}}
}
}));
assert_eq!(closed.schema["required"], json!(["a", "b", "c"]));
assert_eq!(closed.schema["additionalProperties"], json!(false));
assert_eq!(
closed.schema["properties"]["c"]["additionalProperties"],
json!(false)
);
assert_eq!(
closed.forced_required,
vec![
"/properties/b".to_owned(),
"/properties/c".to_owned(),
"/properties/c/properties/d".to_owned()
]
);
}
#[test]
fn inlining_substitutes_a_definition_and_prefers_the_field_documentation() {
let inlined = inline_definitions(
&json!({
"type": "object",
"properties": {"a": {"$ref": "#/$defs/Name", "description": "this field"}},
"$defs": {"Name": {"type": "string", "description": "the type", "minLength": 1}}
}),
DEFAULT_MAX_EXPANSIONS,
)
.expect("acyclic");
assert_eq!(
inlined["properties"]["a"],
json!({"type": "string", "description": "this field", "minLength": 1}),
"the constraint survives and the field's own sentence wins"
);
assert!(inlined.get("$defs").is_none(), "the table is consumed");
}
#[test]
fn inlining_refuses_a_recursive_definition_rather_than_looping() {
let error = inline_definitions(
&json!({
"$ref": "#/$defs/Node",
"$defs": {"Node": {"type": "object", "properties": {"next": {"$ref": "#/$defs/Node"}}}}
}),
DEFAULT_MAX_EXPANSIONS,
)
.expect_err("recursion has no finite expansion");
assert_eq!(error.as_str(), "recursive_ref");
}
#[test]
fn inlining_refuses_a_reference_it_cannot_resolve() {
for reference in [
"https://example.test/schema#/$defs/X",
"#/components/schemas/X",
"#/$defs/Missing",
] {
let error = inline_definitions(&json!({"$ref": reference}), DEFAULT_MAX_EXPANSIONS)
.expect_err("unresolvable");
assert_eq!(error.as_str(), "unresolvable_ref", "{reference}");
}
}
#[test]
fn a_literal_union_collapses_and_keeps_each_variant_sentence() {
let (values, doc) = collapse_literal_union(&[
json!({"const": "plan", "description": "a finished plan"}),
json!({"const": "read_requests", "description": "reads to perform"}),
])
.expect("all branches pin one literal");
assert_eq!(values, vec![json!("plan"), json!("read_requests")]);
assert_eq!(
doc,
"«plan»: a finished plan\n«read_requests»: reads to perform"
);
assert!(collapse_literal_union(&[json!({"type": "string"})]).is_none());
}
#[test]
fn a_dialect_failure_is_a_fallback_not_a_retry() {
use crate::error::{ProviderErrorKind, RetryClass};
let error = ProviderError::from(DialectError::UnprovenUnion {
pointer: String::from("/x"),
keyword: String::from("oneOf"),
});
assert!(matches!(
error.kind(),
ProviderErrorKind::Unsupported { .. }
));
assert_eq!(error.retry_class(), RetryClass::Fallback);
assert_eq!(
error.code().map(|code| code.as_str().to_owned()),
Some("unproven_union".to_owned())
);
}
#[test]
fn an_awkward_property_name_is_escaped_into_the_pointer() {
let error = narrow_unions(&json!({
"properties": {"a/b~c": {"oneOf": [{"type": "object"}, {"type": "object"}]}}
}))
.expect_err("an unprovable union under an awkward name");
assert_eq!(error.pointer(), "/properties/a~1b~0c");
}
#[test]
fn the_limit_counts_expansions_rather_than_json_levels() {
let mut deep = json!({"type": "string"});
for _ in 0..40 {
deep = json!({"type": "array", "items": deep});
}
assert!(
inline_definitions(&deep, 2).is_ok(),
"no references to expand"
);
let chained = json!({
"$ref": "#/$defs/A",
"$defs": {
"A": {"type": "object", "properties": {"b": {"$ref": "#/$defs/B"}}},
"B": {"type": "object", "properties": {"c": {"$ref": "#/$defs/C"}}},
"C": {"type": "string"}
}
});
assert!(inline_definitions(&chained, 8).is_ok());
let error = inline_definitions(&chained, 2).expect_err("three deep");
assert!(
matches!(error, DialectError::TooDeep { limit: 2, .. }),
"{error:?}"
);
}
#[test]
fn a_long_pointer_is_truncated_on_a_character_boundary() {
let deep = format!("/{}", "à".repeat(200));
let error = DialectError::TooDeep {
pointer: short(&deep),
limit: 4,
};
assert!(error.pointer().len() <= MAX_POINTER_LEN + 4);
assert!(error.pointer().ends_with('…'));
}
}