use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum DataType {
Any,
Unknown,
Boolean,
Integer,
Float,
Decimal {
precision: Option<u32>,
scale: Option<i32>,
},
String,
Binary,
Date,
Timestamp {
timezone: Option<String>,
},
List(Box<Field>),
Struct(Vec<Field>),
Map {
key: Box<Field>,
value: Box<Field>,
},
}
impl DataType {
pub fn list(item: DataType) -> Self {
DataType::List(Box::new(Field::new("item", item)))
}
pub fn map(key: DataType, value: DataType) -> Self {
DataType::Map {
key: Box::new(Field::new("key", key).nullable(false)),
value: Box::new(Field::new("value", value)),
}
}
pub fn timestamp() -> Self {
DataType::Timestamp { timezone: None }
}
pub fn decimal() -> Self {
DataType::Decimal {
precision: None,
scale: None,
}
}
pub fn is_numeric(&self) -> bool {
matches!(
self,
DataType::Integer | DataType::Float | DataType::Decimal { .. }
)
}
pub fn is_nested(&self) -> bool {
matches!(
self,
DataType::List(_) | DataType::Struct(_) | DataType::Map { .. }
)
}
pub fn children(&self) -> impl Iterator<Item = &Field> {
let (slice, pair): (&[Field], [Option<&Field>; 2]) = match self {
DataType::Struct(fields) => (fields, [None, None]),
DataType::List(item) => (&[], [Some(item), None]),
DataType::Map { key, value } => (&[], [Some(key), Some(value)]),
_ => (&[], [None, None]),
};
slice.iter().chain(pair.into_iter().flatten())
}
}
impl fmt::Display for DataType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DataType::Any => f.write_str("any"),
DataType::Unknown => f.write_str("unknown"),
DataType::Boolean => f.write_str("boolean"),
DataType::Integer => f.write_str("integer"),
DataType::Float => f.write_str("float"),
DataType::Decimal { precision, scale } => match (precision, scale) {
(Some(p), Some(s)) => write!(f, "decimal({p},{s})"),
(Some(p), None) => write!(f, "decimal({p})"),
_ => f.write_str("decimal"),
},
DataType::String => f.write_str("string"),
DataType::Binary => f.write_str("binary"),
DataType::Date => f.write_str("date"),
DataType::Timestamp { timezone: None } => f.write_str("timestamp"),
DataType::Timestamp { timezone: Some(tz) } => write!(f, "timestamp[{tz}]"),
DataType::List(item) => write!(f, "list<{}>", item.data_type),
DataType::Struct(_) => f.write_str("struct"),
DataType::Map { key, value } => {
write!(f, "map<{}, {}>", key.data_type, value.data_type)
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Literal(String);
impl Literal {
pub fn new(text: impl Into<String>) -> Self {
Literal(text.into())
}
pub fn json(value: &serde_json::Value) -> Self {
Literal(value.to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn as_f64(&self) -> Option<f64> {
let text = self.0.trim();
let numeric = !text.is_empty()
&& text
.chars()
.all(|c| c.is_ascii_digit() || matches!(c, '-' | '+' | '.' | 'e' | 'E'));
if numeric { text.parse().ok() } else { None }
}
pub fn short(&self) -> String {
if self.0.chars().count() > 40 {
let cut: String = self.0.chars().take(39).collect();
format!("{cut}…")
} else {
self.0.clone()
}
}
}
impl fmt::Display for Literal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ForeignKey {
pub columns: Vec<String>,
pub table: String,
pub references: Vec<String>,
}
impl ForeignKey {
pub fn to(table: impl Into<String>, column: impl Into<String>) -> Self {
ForeignKey {
columns: Vec::new(),
table: table.into(),
references: vec![column.into()],
}
}
}
impl fmt::Display for ForeignKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let list = |names: &[String]| match names {
[one] => one.clone(),
many => format!("({})", many.join(", ")),
};
if !self.columns.is_empty() {
write!(f, "{} → ", list(&self.columns))?;
}
if self.references.is_empty() {
f.write_str(&self.table)
} else {
write!(f, "{}.{}", self.table, list(&self.references))
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Constraint {
Const(Literal),
Enum(Vec<Literal>),
Format(String),
Pattern(String),
MinLength(Literal),
MaxLength(Literal),
Minimum(Literal),
ExclusiveMinimum(Literal),
Maximum(Literal),
ExclusiveMaximum(Literal),
MultipleOf(Literal),
MinItems(Literal),
MaxItems(Literal),
UniqueItems(Literal),
Default(Literal),
Deprecated(Literal),
ReadOnly(Literal),
WriteOnly(Literal),
Additional(Literal),
PrimaryKey(Vec<String>),
Unique(Vec<String>),
References(ForeignKey),
Other {
name: String,
value: Literal,
},
}
impl Constraint {
pub fn key(&self) -> &str {
match self {
Constraint::Const(_) => "const",
Constraint::Enum(_) => "enum",
Constraint::Format(_) => "format",
Constraint::Pattern(_) => "pattern",
Constraint::MinLength(_) => "minLength",
Constraint::MaxLength(_) => "maxLength",
Constraint::Minimum(_) => "minimum",
Constraint::ExclusiveMinimum(_) => "exclusiveMinimum",
Constraint::Maximum(_) => "maximum",
Constraint::ExclusiveMaximum(_) => "exclusiveMaximum",
Constraint::MultipleOf(_) => "multipleOf",
Constraint::MinItems(_) => "minItems",
Constraint::MaxItems(_) => "maxItems",
Constraint::UniqueItems(_) => "uniqueItems",
Constraint::Default(_) => "default",
Constraint::Deprecated(_) => "deprecated",
Constraint::ReadOnly(_) => "readOnly",
Constraint::WriteOnly(_) => "writeOnly",
Constraint::Additional(_) => "additionalProperties",
Constraint::PrimaryKey(_) => "primaryKey",
Constraint::Unique(_) => "unique",
Constraint::References(_) => "references",
Constraint::Other { name, .. } => name,
}
}
pub fn literal(&self) -> Option<&Literal> {
match self {
Constraint::Const(v)
| Constraint::MinLength(v)
| Constraint::MaxLength(v)
| Constraint::Minimum(v)
| Constraint::ExclusiveMinimum(v)
| Constraint::Maximum(v)
| Constraint::ExclusiveMaximum(v)
| Constraint::MultipleOf(v)
| Constraint::MinItems(v)
| Constraint::MaxItems(v)
| Constraint::UniqueItems(v)
| Constraint::Default(v)
| Constraint::Deprecated(v)
| Constraint::ReadOnly(v)
| Constraint::WriteOnly(v)
| Constraint::Additional(v)
| Constraint::Other { value: v, .. } => Some(v),
_ => None,
}
}
pub fn name(&self) -> &str {
match self {
Constraint::Additional(_) => "additional properties",
Constraint::PrimaryKey(_) => "primary key",
other => other.key(),
}
}
pub fn value_text(&self) -> String {
match self {
Constraint::Format(format) => format.clone(),
Constraint::Pattern(pattern) => format!("/{pattern}/"),
Constraint::Enum(values) => values
.iter()
.map(Literal::short)
.collect::<Vec<_>>()
.join(", "),
Constraint::Additional(v) => match v.as_str() {
"false" => "refused".into(),
"true" => "allowed".into(),
_ => v.short(),
},
Constraint::PrimaryKey(columns) | Constraint::Unique(columns) => {
if columns.is_empty() {
String::new()
} else {
format!("({})", columns.join(", "))
}
}
Constraint::References(key) => key.to_string(),
other => other.literal().map(Literal::short).unwrap_or_default(),
}
}
}
impl fmt::Display for Constraint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let flag = |v: &Literal| v.as_str() == "true";
match self {
Constraint::Const(v) => write!(f, "= {}", v.short()),
Constraint::Enum(values) => {
let shown: Vec<String> = values.iter().take(8).map(Literal::short).collect();
write!(f, "one of {}", shown.join(", "))?;
if values.len() > 8 {
write!(f, ", … {} more", values.len() - 8)?;
}
Ok(())
}
Constraint::Format(format) => write!(f, "format={format}"),
Constraint::Pattern(pattern) => write!(f, "pattern=/{pattern}/"),
Constraint::UniqueItems(v) if flag(v) => f.write_str("unique items"),
Constraint::Deprecated(v) if flag(v) => f.write_str("deprecated"),
Constraint::ReadOnly(v) if flag(v) => f.write_str("read-only"),
Constraint::WriteOnly(v) if flag(v) => f.write_str("write-only"),
Constraint::UniqueItems(_)
| Constraint::Deprecated(_)
| Constraint::ReadOnly(_)
| Constraint::WriteOnly(_) => Ok(()),
Constraint::Additional(v) if v.as_str() == "false" => {
f.write_str("no other properties")
}
Constraint::Additional(_) => Ok(()),
Constraint::PrimaryKey(columns) if columns.is_empty() => f.write_str("primary key"),
Constraint::PrimaryKey(columns) => write!(f, "primary key ({})", columns.join(", ")),
Constraint::Unique(columns) if columns.is_empty() => f.write_str("unique"),
Constraint::Unique(columns) => write!(f, "unique ({})", columns.join(", ")),
Constraint::References(key) => write!(f, "→ {key}"),
other => write!(
f,
"{}={}",
other.key(),
other.literal().map(Literal::short).unwrap_or_default()
),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Composition {
OneOf,
AnyOf,
AllOf,
}
impl Composition {
pub const ALL: [Composition; 3] = [Composition::OneOf, Composition::AnyOf, Composition::AllOf];
pub fn title(self) -> &'static str {
match self {
Composition::OneOf => "one of",
Composition::AnyOf => "any of",
Composition::AllOf => "all of",
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum FieldKind {
#[default]
Field,
Pattern,
Additional,
TupleItem,
Items,
Group(Composition),
Branch(Composition),
Condition,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Unexpanded {
Recursive(String),
Unresolved {
reference: String,
reason: String,
},
}
impl Unexpanded {
pub fn reference(&self) -> &str {
match self {
Unexpanded::Recursive(reference) => reference,
Unexpanded::Unresolved { reference, .. } => reference,
}
}
}
impl fmt::Display for Unexpanded {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Unexpanded::Recursive(reference) => write!(f, "→ {reference} (recursive)"),
Unexpanded::Unresolved { reference, reason } => write!(f, "→ {reference} ({reason})"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
pub struct Field {
name: String,
data_type: DataType,
nullable: bool,
required: bool,
native_type: Option<String>,
description: Option<String>,
constraints: Vec<Constraint>,
metadata: Vec<(String, String)>,
kind: FieldKind,
reference: Option<String>,
unexpanded: Option<Unexpanded>,
elided: bool,
}
impl Field {
pub fn new(name: impl Into<String>, data_type: DataType) -> Self {
Field {
name: name.into(),
data_type,
nullable: true,
required: false,
native_type: None,
description: None,
constraints: Vec::new(),
metadata: Vec::new(),
kind: FieldKind::Field,
reference: None,
unexpanded: None,
elided: false,
}
}
pub fn nullable(mut self, nullable: bool) -> Self {
self.nullable = nullable;
self
}
pub fn required(mut self, required: bool) -> Self {
self.required = required;
self
}
pub fn with_native_type(mut self, native: impl Into<String>) -> Self {
self.native_type = Some(native.into());
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_type(mut self, data_type: DataType) -> Self {
self.data_type = data_type;
self
}
pub fn with_constraint(mut self, constraint: Constraint) -> Self {
self.constraints.push(constraint);
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.push((key.into(), value.into()));
self
}
pub fn with_kind(mut self, kind: FieldKind) -> Self {
self.kind = kind;
self
}
pub fn with_reference(mut self, reference: impl Into<String>) -> Self {
self.reference = Some(reference.into());
self
}
pub fn with_unexpanded(mut self, unexpanded: Unexpanded) -> Self {
self.unexpanded = Some(unexpanded);
self
}
pub fn elided(mut self, elided: bool) -> Self {
self.elided = elided;
self
}
pub fn name(&self) -> &str {
&self.name
}
pub fn data_type(&self) -> &DataType {
&self.data_type
}
pub fn data_type_mut(&mut self) -> &mut DataType {
&mut self.data_type
}
pub fn is_nullable(&self) -> bool {
self.nullable
}
pub fn is_required(&self) -> bool {
self.required
}
pub fn native_type(&self) -> Option<&str> {
self.native_type.as_deref()
}
pub fn type_label(&self) -> String {
self.native_type
.clone()
.unwrap_or_else(|| self.data_type.to_string())
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
pub fn constraints(&self) -> &[Constraint] {
&self.constraints
}
pub fn constraints_mut(&mut self) -> &mut Vec<Constraint> {
&mut self.constraints
}
pub fn constraint(&self, key: &str) -> Option<&Constraint> {
self.constraints.iter().find(|c| c.key() == key)
}
pub fn metadata(&self) -> &[(String, String)] {
&self.metadata
}
pub fn enum_values(&self) -> Option<&[Literal]> {
self.constraints.iter().find_map(|c| match c {
Constraint::Enum(values) => Some(values.as_slice()),
_ => None,
})
}
pub fn default_value(&self) -> Option<&Literal> {
self.constraints.iter().find_map(|c| match c {
Constraint::Default(value) => Some(value),
_ => None,
})
}
pub fn is_primary_key(&self) -> bool {
self.constraints
.iter()
.any(|c| matches!(c, Constraint::PrimaryKey(_)))
}
pub fn is_unique(&self) -> bool {
self.constraints
.iter()
.any(|c| matches!(c, Constraint::Unique(columns) if columns.is_empty()))
}
pub fn references(&self) -> Option<&ForeignKey> {
self.constraints.iter().find_map(|c| match c {
Constraint::References(key) => Some(key),
_ => None,
})
}
pub fn kind(&self) -> FieldKind {
self.kind
}
pub fn reference(&self) -> Option<&str> {
self.reference.as_deref()
}
pub fn unexpanded(&self) -> Option<&Unexpanded> {
self.unexpanded.as_ref()
}
pub fn is_elided(&self) -> bool {
self.elided
}
pub fn children(&self) -> impl Iterator<Item = &Field> {
self.data_type.children()
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
pub struct Schema {
name: Option<String>,
description: Option<String>,
fields: Vec<Field>,
constraints: Vec<Constraint>,
tables: Vec<Schema>,
metadata: Vec<(String, String)>,
truncated: bool,
}
impl Schema {
pub fn new(fields: impl IntoIterator<Item = Field>) -> Self {
Schema {
fields: fields.into_iter().collect(),
..Schema::default()
}
}
pub fn of_tables(tables: impl IntoIterator<Item = Schema>) -> Self {
Schema {
tables: tables.into_iter().collect(),
..Schema::default()
}
}
pub fn named(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_constraint(mut self, constraint: Constraint) -> Self {
self.constraints.push(constraint);
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.push((key.into(), value.into()));
self
}
pub fn truncated(mut self, truncated: bool) -> Self {
self.truncated = truncated;
self
}
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
pub fn fields(&self) -> &[Field] {
&self.fields
}
pub fn fields_mut(&mut self) -> &mut Vec<Field> {
&mut self.fields
}
pub fn push(&mut self, field: Field) -> &mut Self {
self.fields.push(field);
self
}
pub fn field(&self, name: &str) -> Option<&Field> {
self.fields.iter().find(|f| f.name == name)
}
pub fn index_of(&self, name: &str) -> Option<usize> {
self.fields.iter().position(|f| f.name == name)
}
pub fn len(&self) -> usize {
self.fields.len()
}
pub fn is_empty(&self) -> bool {
self.fields.is_empty()
}
pub fn constraints(&self) -> &[Constraint] {
&self.constraints
}
pub fn constraints_mut(&mut self) -> &mut Vec<Constraint> {
&mut self.constraints
}
pub fn primary_key(&self) -> Vec<&str> {
let table = self.constraints.iter().find_map(|c| match c {
Constraint::PrimaryKey(columns) if !columns.is_empty() => Some(columns),
_ => None,
});
match table {
Some(columns) => columns.iter().map(String::as_str).collect(),
None => self
.fields
.iter()
.filter(|f| f.is_primary_key())
.map(Field::name)
.collect(),
}
}
pub fn foreign_keys(&self) -> Vec<ForeignKey> {
let table = self.constraints.iter().filter_map(|c| match c {
Constraint::References(key) => Some(key.clone()),
_ => None,
});
let columns = self.fields.iter().filter_map(|f| {
f.references().map(|key| ForeignKey {
columns: vec![f.name.clone()],
..key.clone()
})
});
table.chain(columns).collect()
}
pub fn tables(&self) -> &[Schema] {
&self.tables
}
pub fn tables_mut(&mut self) -> &mut Vec<Schema> {
&mut self.tables
}
pub fn push_table(&mut self, table: Schema) -> &mut Self {
self.tables.push(table);
self
}
pub fn table(&self, name: &str) -> Option<&Schema> {
self.tables.iter().find(|t| t.name() == Some(name))
}
pub fn metadata(&self) -> &[(String, String)] {
&self.metadata
}
pub fn is_truncated(&self) -> bool {
self.truncated
}
}
impl FromIterator<Field> for Schema {
fn from_iter<I: IntoIterator<Item = Field>>(iter: I) -> Self {
Schema::new(iter)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn types_display_with_their_parameters() {
let cases = [
(DataType::Any, "any"),
(DataType::Unknown, "unknown"),
(DataType::Boolean, "boolean"),
(DataType::Float, "float"),
(DataType::decimal(), "decimal"),
(
DataType::Decimal {
precision: Some(10),
scale: Some(2),
},
"decimal(10,2)",
),
(
DataType::Decimal {
precision: Some(38),
scale: None,
},
"decimal(38)",
),
(DataType::Binary, "binary"),
(DataType::Date, "date"),
(DataType::timestamp(), "timestamp"),
(
DataType::Timestamp {
timezone: Some("UTC".into()),
},
"timestamp[UTC]",
),
(
DataType::list(DataType::list(DataType::Integer)),
"list<list<integer>>",
),
(
DataType::map(DataType::String, DataType::Float),
"map<string, float>",
),
(DataType::Struct(vec![]), "struct"),
];
for (data_type, expected) in cases {
assert_eq!(data_type.to_string(), expected);
}
}
#[test]
fn children_cover_structs_lists_and_maps() {
let map = DataType::map(DataType::String, DataType::Integer);
let names: Vec<&str> = map.children().map(Field::name).collect();
assert_eq!(names, ["key", "value"]);
assert!(!map.children().next().unwrap().is_nullable());
let list = DataType::list(DataType::Date);
assert_eq!(list.children().count(), 1);
assert_eq!(DataType::String.children().count(), 0);
assert!(DataType::Struct(vec![]).is_nested());
assert!(DataType::decimal().is_numeric());
assert!(!DataType::String.is_numeric());
}
#[test]
fn fields_carry_native_types_and_descriptions() {
let field = Field::new("id", DataType::Integer)
.nullable(false)
.with_native_type("Int32")
.with_description("primary key");
assert_eq!(field.name(), "id");
assert!(!field.is_nullable());
assert_eq!(field.native_type(), Some("Int32"));
assert_eq!(field.description(), Some("primary key"));
let schema: Schema = [field.clone()].into_iter().collect();
let schema = schema.named("users");
assert_eq!(schema.name(), Some("users"));
assert_eq!(schema.field("id"), Some(&field));
assert_eq!(schema.index_of("missing"), None);
assert_eq!(schema.len(), 1);
assert!(!schema.is_empty());
}
#[test]
fn constraints_show_compare_and_name_themselves() {
let lit = Literal::new;
let cases = [
(Constraint::Const(lit("5")), "= 5", "const", "5"),
(
Constraint::Enum(vec![lit("\"a\""), lit("1")]),
"one of \"a\", 1",
"enum",
"\"a\", 1",
),
(
Constraint::Format("email".into()),
"format=email",
"format",
"email",
),
(
Constraint::Pattern("^a".into()),
"pattern=/^a/",
"pattern",
"/^a/",
),
(
Constraint::MinLength(lit("1")),
"minLength=1",
"minLength",
"1",
),
(
Constraint::UniqueItems(lit("true")),
"unique items",
"uniqueItems",
"true",
),
(
Constraint::UniqueItems(lit("false")),
"",
"uniqueItems",
"false",
),
(
Constraint::Deprecated(lit("true")),
"deprecated",
"deprecated",
"true",
),
(
Constraint::Additional(lit("false")),
"no other properties",
"additional properties",
"refused",
),
(
Constraint::Additional(lit("true")),
"",
"additional properties",
"allowed",
),
(
Constraint::PrimaryKey(vec![]),
"primary key",
"primary key",
"",
),
(
Constraint::PrimaryKey(vec!["a".into(), "b".into()]),
"primary key (a, b)",
"primary key",
"(a, b)",
),
(Constraint::Unique(vec![]), "unique", "unique", ""),
(
Constraint::References(ForeignKey::to("users", "id")),
"→ users.id",
"references",
"users.id",
),
(
Constraint::Other {
name: "minContains".into(),
value: lit("2"),
},
"minContains=2",
"minContains",
"2",
),
];
for (constraint, shown, name, value) in cases {
assert_eq!(constraint.to_string(), shown, "{constraint:?}");
assert_eq!(constraint.name(), name, "{constraint:?}");
assert_eq!(constraint.value_text(), value, "{constraint:?}");
}
let long = Literal::new("x".repeat(50));
assert_eq!(long.short().chars().count(), 40);
assert!(long.short().ends_with('…'));
assert_eq!(Literal::new("1.5e3").as_f64(), Some(1500.0));
assert_eq!(Literal::new("\"3\"").as_f64(), None);
assert_eq!(Literal::new("inf").as_f64(), None);
assert_eq!(
Literal::json(&serde_json::json!({"a": [1]})).as_str(),
r#"{"a":[1]}"#
);
}
#[test]
fn schemas_hold_tables_and_keys() {
let line = Schema::new([
Field::new("order", DataType::Integer)
.with_constraint(Constraint::References(ForeignKey::to("orders", "id"))),
Field::new("n", DataType::Integer),
])
.named("lines")
.with_constraint(Constraint::PrimaryKey(vec!["order".into(), "n".into()]))
.with_constraint(Constraint::References(ForeignKey {
columns: vec!["order".into(), "n".into()],
table: "x".into(),
references: vec![],
}));
assert_eq!(line.primary_key(), ["order", "n"]);
let keys: Vec<String> = line
.foreign_keys()
.iter()
.map(ToString::to_string)
.collect();
assert_eq!(keys, ["(order, n) → x", "order → orders.id"]);
let mut all = Schema::of_tables([line.clone()]);
all.push_table(Schema::new([]).named("orders"));
assert_eq!(all.tables().len(), 2);
assert_eq!(all.table("lines"), Some(&line));
assert!(all.is_empty());
assert!(all.truncated(true).is_truncated());
}
#[test]
fn unexpanded_references_say_why() {
assert_eq!(
Unexpanded::Recursive("#/$defs/n".into()).to_string(),
"→ #/$defs/n (recursive)"
);
let unresolved = Unexpanded::Unresolved {
reference: "a.json".into(),
reason: "another document".into(),
};
assert_eq!(unresolved.to_string(), "→ a.json (another document)");
assert_eq!(unresolved.reference(), "a.json");
assert_eq!(Composition::AnyOf.title(), "any of");
}
}