use std::collections::{BTreeMap, BTreeSet, HashSet};
use heck::ToPascalCase;
use log::debug;
use schemars::schema::{
ArrayValidation, InstanceType, Metadata, ObjectValidation, Schema, SchemaObject, SingleOrVec,
StringValidation, SubschemaValidation,
};
use unicode_ident::{is_xid_continue, is_xid_start};
use crate::{validate::schema_value_validate, Error, Name, RefKey, Result, TypeSpace};
pub(crate) fn metadata_description(metadata: &Option<Box<Metadata>>) -> Option<String> {
metadata
.as_ref()
.and_then(|metadata| metadata.description.as_ref().cloned())
}
pub(crate) fn metadata_title(metadata: &Option<Box<Metadata>>) -> Option<String> {
metadata
.as_ref()
.and_then(|metadata| metadata.title.as_ref().cloned())
}
pub(crate) fn metadata_title_and_description(metadata: &Option<Box<Metadata>>) -> Option<String> {
metadata
.as_ref()
.and_then(|metadata| match (&metadata.title, &metadata.description) {
(Some(t), Some(d)) => Some(format!("{}\n\n{}", t, d)),
(Some(t), None) => Some(t.clone()),
(None, Some(d)) => Some(d.clone()),
(None, None) => None,
})
}
pub(crate) fn all_mutually_exclusive(
subschemas: &[Schema],
definitions: &BTreeMap<RefKey, Schema>,
) -> bool {
let len = subschemas.len();
(0..len - 1)
.flat_map(|ii| (ii + 1..len).map(move |jj| (ii, jj)))
.all(|(ii, jj)| {
let a = resolve(&subschemas[ii], definitions);
let b = resolve(&subschemas[jj], definitions);
schemas_mutually_exclusive(a, b, definitions)
})
}
fn schemas_mutually_exclusive(
a: &Schema,
b: &Schema,
definitions: &BTreeMap<RefKey, Schema>,
) -> bool {
match (a, b) {
(Schema::Bool(false), _) => true,
(_, Schema::Bool(false)) => true,
(Schema::Bool(true), _) => false,
(_, Schema::Bool(true)) => false,
(
other,
Schema::Object(SchemaObject {
metadata: None,
instance_type: None,
format: None,
enum_values: None,
const_value: None,
subschemas: Some(subschemas),
number: None,
string: None,
array: None,
object: None,
reference: None,
extensions: _,
}),
)
| (
Schema::Object(SchemaObject {
metadata: None,
instance_type: None,
format: None,
enum_values: None,
const_value: None,
subschemas: Some(subschemas),
number: None,
string: None,
array: None,
object: None,
reference: None,
extensions: _,
}),
other,
) => match subschemas.as_ref() {
SubschemaValidation {
all_of: Some(s),
any_of: None,
one_of: None,
not: None,
if_schema: None,
then_schema: None,
else_schema: None,
} => s
.iter()
.any(|sub| schemas_mutually_exclusive(sub, other, definitions)),
SubschemaValidation {
all_of: None,
any_of: Some(s),
one_of: None,
not: None,
if_schema: None,
then_schema: None,
else_schema: None,
}
| SubschemaValidation {
all_of: None,
any_of: None,
one_of: Some(s),
not: None,
if_schema: None,
then_schema: None,
else_schema: None,
} => s
.iter()
.all(|sub| schemas_mutually_exclusive(sub, other, definitions)),
SubschemaValidation {
all_of: None,
any_of: None,
one_of: None,
not: Some(sub),
if_schema: None,
then_schema: None,
else_schema: None,
} => !schemas_mutually_exclusive(sub, other, definitions),
_ => false,
},
(
schema,
Schema::Object(SchemaObject {
instance_type: None,
enum_values: Some(enum_values),
..
}),
)
| (
Schema::Object(SchemaObject {
instance_type: None,
enum_values: Some(enum_values),
..
}),
schema,
) => enum_values
.iter()
.all(|value| schema_value_validate(schema, value, definitions).is_err()),
(
schema,
Schema::Object(SchemaObject {
const_value: Some(value),
..
}),
)
| (
Schema::Object(SchemaObject {
const_value: Some(value),
..
}),
schema,
) => schema_value_validate(schema, value, definitions).is_err(),
(Schema::Object(a), Schema::Object(b)) => {
match (&a.instance_type, &b.instance_type) {
(None, _) => false,
(_, None) => false,
(Some(SingleOrVec::Single(a_single)), Some(SingleOrVec::Single(b_single)))
if a_single != b_single =>
{
true
}
(Some(SingleOrVec::Single(a_single)), Some(SingleOrVec::Single(b_single)))
if a_single == b_single && a_single.as_ref() == &InstanceType::Object =>
{
if let (
SchemaObject {
metadata: _,
instance_type: _,
format: None,
enum_values: None,
const_value: None,
subschemas: None,
number: None,
string: None,
array: None,
object: Some(a_validation),
reference: None,
extensions: _,
},
SchemaObject {
metadata: _,
instance_type: _,
format: None,
enum_values: None,
const_value: None,
subschemas: None,
number: None,
string: None,
array: None,
object: Some(b_validation),
reference: None,
extensions: _,
},
) = (a, b)
{
object_schemas_mutually_exclusive(a_validation, b_validation)
} else {
false
}
}
(Some(SingleOrVec::Single(a_single)), Some(SingleOrVec::Single(b_single)))
if a_single == b_single && a_single.as_ref() == &InstanceType::Array =>
{
if let (
SchemaObject {
metadata: _,
instance_type: _,
format: None,
enum_values: None,
const_value: None,
subschemas: None,
number: None,
string: None,
array: Some(a_validation),
object: None,
reference: None,
extensions: _,
},
SchemaObject {
metadata: _,
instance_type: _,
format: None,
enum_values: None,
const_value: None,
subschemas: None,
number: None,
string: None,
array: Some(b_validation),
object: None,
reference: None,
extensions: _,
},
) = (a, b)
{
array_schemas_mutually_exclusive(a_validation, b_validation, definitions)
} else {
false
}
}
(Some(SingleOrVec::Single(a_single)), Some(SingleOrVec::Single(b_single))) => {
a_single != b_single
}
(Some(SingleOrVec::Vec(a_vec)), Some(SingleOrVec::Vec(b_vec))) => a_vec
.iter()
.all(|instance_type| !b_vec.contains(instance_type)),
(Some(SingleOrVec::Single(single)), Some(SingleOrVec::Vec(vec)))
| (Some(SingleOrVec::Vec(vec)), Some(SingleOrVec::Single(single))) => {
!vec.contains(single)
}
}
}
}
}
fn object_schemas_mutually_exclusive(
a_validation: &ObjectValidation,
b_validation: &ObjectValidation,
) -> bool {
let ObjectValidation {
required: a_required,
properties: a_properties,
..
} = a_validation;
let ObjectValidation {
required: b_required,
properties: b_properties,
..
} = b_validation;
if a_properties.is_empty() || b_properties.is_empty() {
return false;
}
if !a_required.is_subset(&b_properties.keys().cloned().collect())
|| !b_required.is_subset(&a_properties.keys().cloned().collect())
{
true
} else {
let aa = a_required
.iter()
.filter_map(|name| {
let t = a_properties.get(name).unwrap();
constant_string_value(t).map(|s| (name.clone(), s))
})
.collect::<HashSet<_>>();
let bb = b_required
.iter()
.filter_map(|name| {
let t = b_properties.get(name).unwrap();
constant_string_value(t).map(|s| (name.clone(), s))
})
.collect::<HashSet<_>>();
!aa.is_subset(&bb) && !bb.is_subset(&aa)
}
}
fn array_schemas_mutually_exclusive(
a_validation: &ArrayValidation,
b_validation: &ArrayValidation,
definitions: &BTreeMap<RefKey, Schema>,
) -> bool {
match (a_validation, b_validation) {
(
ArrayValidation {
items: Some(SingleOrVec::Single(single)),
additional_items: None,
..
},
ArrayValidation {
items: Some(SingleOrVec::Vec(vec)),
additional_items: None,
max_items: Some(max_items),
min_items: Some(min_items),
unique_items: None,
contains: None,
},
)
| (
ArrayValidation {
items: Some(SingleOrVec::Vec(vec)),
additional_items: None,
max_items: Some(max_items),
min_items: Some(min_items),
unique_items: None,
contains: None,
},
ArrayValidation {
items: Some(SingleOrVec::Single(single)),
additional_items: None,
..
},
) if max_items == min_items && *max_items as usize == vec.len() => vec
.iter()
.any(|schema| schemas_mutually_exclusive(schema, single, definitions)),
(aa, bb) => {
match (&aa.max_items, &bb.min_items) {
(Some(max), Some(min)) if min > max => return true,
_ => (),
}
match (&bb.max_items, &aa.min_items) {
(Some(max), Some(min)) if min > max => return true,
_ => (),
}
match (&aa.items, &aa.max_items, &bb.items, &bb.max_items) {
(Some(SingleOrVec::Single(a_items)), _, Some(SingleOrVec::Single(b_items)), _)
if schemas_mutually_exclusive(a_items, b_items, definitions) =>
{
return true;
}
_ => (),
}
debug!(
"giving up on mutual exclusivity check {} {}",
serde_json::to_string_pretty(aa).unwrap(),
serde_json::to_string_pretty(bb).unwrap(),
);
false
}
}
}
pub(crate) fn constant_string_value(schema: &Schema) -> Option<&str> {
match schema {
Schema::Object(SchemaObject {
metadata: _,
instance_type: Some(SingleOrVec::Single(single)),
format: None,
enum_values: Some(values),
const_value: None,
subschemas: None,
number: None,
string: None,
array: None,
object: None,
reference: None,
extensions: _,
}) if single.as_ref() == &InstanceType::String && values.len() == 1 => {
values.first().unwrap().as_str()
}
Schema::Object(SchemaObject {
metadata: _,
instance_type: None,
format: None,
enum_values: Some(values),
const_value: None,
subschemas: None,
number: None,
string: None,
array: None,
object: None,
reference: None,
extensions: _,
}) if values.len() == 1 => values.first().unwrap().as_str(),
Schema::Object(SchemaObject {
metadata: _,
instance_type: Some(SingleOrVec::Single(single)),
format: None,
enum_values: None,
const_value: Some(value),
subschemas: None,
number: None,
string: None,
array: None,
object: None,
reference: None,
extensions: _,
}) if single.as_ref() == &InstanceType::String => value.as_str(),
Schema::Object(SchemaObject {
metadata: _,
instance_type: None,
format: None,
enum_values: None,
const_value: Some(value),
subschemas: None,
number: None,
string: None,
array: None,
object: None,
reference: None,
extensions: _,
}) => value.as_str(),
_ => None,
}
}
fn decode_segment(segment: &str) -> String {
segment.replace("~1", "/").replace("~0", "~")
}
pub(crate) fn ref_key(ref_name: &str) -> RefKey {
if ref_name == "#" {
RefKey::Root
} else if let Some(idx) = ref_name.rfind('/') {
let decoded_segment = decode_segment(&ref_name[idx + 1..]);
RefKey::Def(decoded_segment)
} else {
panic!("expected a '/' in $ref: {}", ref_name)
}
}
fn resolve<'a>(
schema: &'a Schema,
definitions: &'a std::collections::BTreeMap<RefKey, Schema>,
) -> &'a Schema {
match schema {
Schema::Bool(_) => schema,
Schema::Object(SchemaObject {
metadata: _,
instance_type: None,
format: None,
enum_values: None,
const_value: None,
subschemas: None,
number: None,
string: None,
array: None,
object: None,
reference: Some(ref_name),
extensions: _,
}) => definitions.get(&ref_key(ref_name)).unwrap(),
Schema::Object(SchemaObject {
reference: None, ..
}) => schema,
_ => todo!(),
}
}
pub(crate) fn schema_is_named(schema: &Schema) -> Option<String> {
let raw_name = match schema {
Schema::Object(SchemaObject {
metadata: _,
instance_type: None,
format: None,
enum_values: None,
const_value: None,
subschemas: None,
number: None,
string: None,
array: None,
object: None,
reference: Some(reference),
extensions: _,
}) => {
let idx = reference.rfind('/')?;
Some(reference[idx + 1..].to_string())
}
Schema::Object(SchemaObject {
metadata: Some(metadata),
..
}) if metadata.as_ref().title.is_some() => Some(metadata.as_ref().title.as_ref()?.clone()),
Schema::Object(SchemaObject {
metadata: _,
instance_type: _,
format: None,
enum_values: None,
const_value: None,
subschemas: Some(subschemas),
number: None,
string: None,
array: None,
object: None,
reference: None,
extensions: _,
}) => singleton_subschema(subschemas).and_then(schema_is_named),
Schema::Object(SchemaObject {
instance_type: Some(SingleOrVec::Single(single)),
format,
..
}) => match (**single, format.as_deref()) {
(_, Some(format)) => Some(format.to_pascal_case()),
(InstanceType::Boolean, _) => Some("Boolean".to_string()),
(InstanceType::Integer, _) => Some("Integer".to_string()),
(InstanceType::Number, _) => Some("Number".to_string()),
(InstanceType::String, _) => Some("String".to_string()),
(InstanceType::Array, _) => Some("Array".to_string()),
(InstanceType::Object, _) => Some("Object".to_string()),
(InstanceType::Null, _) => Some("Null".to_string()),
},
_ => None,
}?;
Some(sanitize(&raw_name, Case::Pascal))
}
pub(crate) fn get_object(schema: &Schema) -> Option<(&Option<Box<Metadata>>, &ObjectValidation)> {
match schema {
Schema::Object(SchemaObject {
metadata,
instance_type: Some(SingleOrVec::Single(single)),
format: None,
enum_values: None,
const_value: None,
subschemas: None,
number: _,
string: _,
array: _,
object: Some(validation),
reference: None,
extensions: _,
}) if single.as_ref() == &InstanceType::Object
&& schema_none_or_false(&validation.additional_properties)
&& validation.max_properties.is_none()
&& validation.min_properties.is_none()
&& validation.pattern_properties.is_empty()
&& validation.property_names.is_none() =>
{
Some((metadata, validation.as_ref()))
}
Schema::Object(SchemaObject {
metadata,
instance_type: None,
format: None,
enum_values: None,
const_value: None,
subschemas: None,
number: None,
string: None,
array: None,
object: Some(validation),
reference: None,
extensions: _,
}) if schema_none_or_false(&validation.additional_properties)
&& validation.max_properties.is_none()
&& validation.min_properties.is_none()
&& validation.pattern_properties.is_empty()
&& validation.property_names.is_none() =>
{
Some((metadata, validation.as_ref()))
}
Schema::Object(SchemaObject {
metadata,
instance_type: _,
format: None,
enum_values: None,
const_value: None,
subschemas: Some(subschemas),
number: None,
string: None,
array: None,
object: None,
reference: None,
extensions: _,
}) => singleton_subschema(subschemas).and_then(|sub_schema| {
get_object(sub_schema).map(|(m, validation)| match m {
Some(_) => (metadata, validation),
None => (&None, validation),
})
}),
_ => None,
}
}
fn schema_none_or_false(additional_properties: &Option<Box<Schema>>) -> bool {
matches!(
additional_properties.as_ref().map(Box::as_ref),
None | Some(Schema::Bool(false))
)
}
pub(crate) fn singleton_subschema(subschemas: &SubschemaValidation) -> Option<&Schema> {
match subschemas {
SubschemaValidation {
all_of: Some(subschemas),
any_of: None,
one_of: None,
not: None,
if_schema: None,
then_schema: None,
else_schema: None,
}
| SubschemaValidation {
all_of: None,
any_of: Some(subschemas),
one_of: None,
not: None,
if_schema: None,
then_schema: None,
else_schema: None,
}
| SubschemaValidation {
all_of: None,
any_of: None,
one_of: Some(subschemas),
not: None,
if_schema: None,
then_schema: None,
else_schema: None,
} if subschemas.len() == 1 => subschemas.first(),
_ => None,
}
}
pub(crate) enum Case {
Pascal,
Snake,
}
pub(crate) fn sanitize(input: &str, case: Case) -> String {
use heck::{ToPascalCase, ToSnakeCase};
let to_case = match case {
Case::Pascal => str::to_pascal_case,
Case::Snake => str::to_snake_case,
};
let out = match input {
"+1" => "plus1".to_string(),
"-1" => "minus1".to_string(),
_ => to_case(&input.replace("'", "").replace(|c| !is_xid_continue(c), "-")),
};
let prefix = to_case("x");
let out = match out.chars().next() {
None => prefix,
Some(c) if is_xid_start(c) => out,
Some(_) => format!("{}{}", prefix, out),
};
if accept_as_ident(&out) {
out
} else {
format!("{}_", out)
}
}
pub fn accept_as_ident(ident: &str) -> bool {
match ident {
"_" |
"abstract" | "as" | "async" | "await" | "become" | "box" | "break" |
"const" | "continue" | "crate" | "do" | "dyn" | "else" | "enum" |
"extern" | "false" | "final" | "fn" | "for" | "gen" | "if" | "impl" |
"in" | "let" | "loop" | "macro" | "match" | "mod" | "move" | "mut" |
"override" | "priv" | "pub" | "ref" | "return" | "Self" | "self" |
"static" | "struct" | "super" | "trait" | "true" | "try" | "type" |
"typeof" | "unsafe" | "unsized" | "use" | "virtual" | "where" |
"while" | "yield" => false,
_ => true,
}
}
pub(crate) fn recase(input: &str, case: Case) -> (String, Option<String>) {
let new = sanitize(input, case);
let rename = if new == input {
None
} else {
Some(input.to_string())
};
(new, rename)
}
pub(crate) fn unique<I, T>(items: I) -> bool
where
I: IntoIterator<Item = T>,
T: Eq + std::hash::Hash,
{
let mut unique = HashSet::new();
items.into_iter().all(|item| unique.insert(item))
}
pub(crate) fn get_type_name(type_name: &Name, metadata: &Option<Box<Metadata>>) -> Option<String> {
let name = match (type_name, metadata_title(metadata)) {
(Name::Required(name), _) => name.clone(),
(Name::Suggested(name), None) => name.clone(),
(_, Some(name)) => name,
(Name::Unknown, None) => None?,
};
Some(sanitize(&name, Case::Pascal))
}
pub(crate) struct TypePatch {
pub name: String,
pub derives: BTreeSet<String>,
pub attrs: BTreeSet<String>,
}
impl TypePatch {
pub fn new(type_space: &TypeSpace, type_name: String) -> Self {
match type_space.settings.patch.get(&type_name) {
None => Self {
name: type_name,
derives: Default::default(),
attrs: Default::default(),
},
Some(patch) => {
let name = patch.rename.clone().unwrap_or(type_name);
let derives = patch.derives.iter().cloned().collect();
let attrs = patch.attrs.iter().cloned().collect();
Self {
name,
derives,
attrs,
}
}
}
}
}
pub(crate) struct StringValidator {
max_length: Option<u32>,
min_length: Option<u32>,
pattern: Option<regress::Regex>,
}
impl StringValidator {
pub fn new(type_name: &Name, validation: Option<&StringValidation>) -> Result<Self> {
let (max_length, min_length, pattern) =
validation.map_or(Ok((None, None, None)), |validation| {
let max = validation.max_length;
let min = validation.min_length;
let pattern = validation
.pattern
.as_ref()
.map(|pattern| {
regress::Regex::new(pattern).map_err(|e| Error::InvalidSchema {
type_name: type_name.clone().into_option(),
reason: format!("invalid pattern '{}' {}", pattern, e),
})
})
.transpose()?;
Ok((max, min, pattern))
})?;
Ok(Self {
max_length,
min_length,
pattern,
})
}
pub fn is_valid<S: AsRef<str>>(&self, s: S) -> bool {
self.max_length
.as_ref()
.is_none_or(|max| s.as_ref().len() as u32 <= *max)
&& self
.min_length
.as_ref()
.is_none_or(|min| s.as_ref().len() as u32 >= *min)
&& self
.pattern
.as_ref()
.is_none_or(|pattern| pattern.find(s.as_ref()).is_some())
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use schemars::{
gen::{SchemaGenerator, SchemaSettings},
schema::StringValidation,
schema_for, JsonSchema,
};
use crate::{
util::{decode_segment, sanitize, schemas_mutually_exclusive, Case},
Name,
};
use super::StringValidator;
#[test]
fn test_non_exclusive_structs() {
#![allow(dead_code)]
#[derive(JsonSchema)]
struct A {
a: Option<()>,
b: (),
}
#[derive(JsonSchema)]
struct B {
a: (),
b: Option<()>,
}
let a = schema_for!(A).schema.into();
let b = schema_for!(B).schema.into();
assert!(!schemas_mutually_exclusive(&a, &b, &BTreeMap::new()));
assert!(!schemas_mutually_exclusive(&b, &a, &BTreeMap::new()));
}
#[test]
fn test_non_exclusive_oneof_subschema() {
#![allow(dead_code)]
#[derive(JsonSchema)]
enum A {
B(i32),
C(i64),
}
let mut settings = SchemaSettings::default();
settings.inline_subschemas = true;
let gen = SchemaGenerator::new(settings);
let a = gen.into_root_schema_for::<Vec<A>>().schema.into();
assert!(!schemas_mutually_exclusive(&a, &a, &BTreeMap::new()));
}
#[test]
fn test_unique_prop_structs() {
#![allow(dead_code)]
#[derive(JsonSchema)]
struct A {
a: Option<()>,
b: (),
}
#[derive(JsonSchema)]
struct B {
a: (),
b: Option<()>,
c: (),
}
let a = schema_for!(A).schema.into();
let b = schema_for!(B).schema.into();
assert!(schemas_mutually_exclusive(&a, &b, &BTreeMap::new()));
assert!(schemas_mutually_exclusive(&b, &a, &BTreeMap::new()));
}
#[test]
fn test_exclusive_structs() {
#![allow(dead_code)]
#[derive(JsonSchema)]
struct A {
a: Option<()>,
b: (),
aa: (),
}
#[derive(JsonSchema)]
struct B {
a: (),
b: Option<()>,
bb: (),
}
let a = schema_for!(A).schema.into();
let b = schema_for!(B).schema.into();
assert!(schemas_mutually_exclusive(&a, &b, &BTreeMap::new()));
assert!(schemas_mutually_exclusive(&b, &a, &BTreeMap::new()));
}
#[test]
fn test_exclusive_simple_arrays() {
let a = schema_for!(Vec<u32>).schema.into();
let b = schema_for!(Vec<f32>).schema.into();
assert!(schemas_mutually_exclusive(&a, &b, &BTreeMap::new()));
assert!(schemas_mutually_exclusive(&b, &a, &BTreeMap::new()));
}
#[test]
fn test_decode_segment() {
assert_eq!(decode_segment("foo~1bar"), "foo/bar");
assert_eq!(decode_segment("foo~0bar"), "foo~bar");
}
#[test]
fn test_sanitize() {
assert_eq!(sanitize("type", Case::Snake), "type_");
assert_eq!(sanitize("ref", Case::Snake), "ref_");
assert_eq!(sanitize("gen", Case::Snake), "gen_");
assert_eq!(sanitize("gen", Case::Pascal), "Gen");
assert_eq!(sanitize("+1", Case::Snake), "plus1");
assert_eq!(sanitize("-1", Case::Snake), "minus1");
assert_eq!(sanitize("@timestamp", Case::Pascal), "Timestamp");
assert_eq!(sanitize("won't and can't", Case::Pascal), "WontAndCant");
assert_eq!(
sanitize(
"urn:ietf:params:scim:schemas:extension:gluu:2.0:user_",
Case::Pascal
),
"UrnIetfParamsScimSchemasExtensionGluu20User"
);
assert_eq!(sanitize("Ipv6Net", Case::Snake), "ipv6_net");
assert_eq!(sanitize("V6", Case::Pascal), "V6");
}
#[test]
fn test_string_validation() {
let permissive = StringValidator::new(&Name::Unknown, None).unwrap();
assert!(permissive.is_valid("everything should be fine"));
assert!(permissive.is_valid(""));
let also_permissive = StringValidator::new(
&Name::Unknown,
Some(&StringValidation {
max_length: None,
min_length: None,
pattern: None,
}),
)
.unwrap();
assert!(also_permissive.is_valid("everything should be fine"));
assert!(also_permissive.is_valid(""));
let eight = StringValidator::new(
&Name::Unknown,
Some(&StringValidation {
max_length: Some(8),
min_length: Some(8),
pattern: None,
}),
)
.unwrap();
assert!(eight.is_valid("Shadrach"));
assert!(!eight.is_valid("Meshach"));
assert!(eight.is_valid("Abednego"));
let ach = StringValidator::new(
&Name::Unknown,
Some(&StringValidation {
max_length: None,
min_length: None,
pattern: Some("ach$".to_string()),
}),
)
.unwrap();
assert!(ach.is_valid("Shadrach"));
assert!(ach.is_valid("Meshach"));
assert!(!ach.is_valid("Abednego"));
}
}