#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JsonKind {
Null,
Bool,
Number,
String,
Array,
Object,
Unknown,
}
pub fn looks_like_json(input: &str) -> bool {
detect_json_kind(input) != JsonKind::Unknown
}
pub fn looks_like_json_object(input: &str) -> bool {
let trimmed = input.trim();
trimmed.len() >= 2 && trimmed.starts_with('{') && trimmed.ends_with('}')
}
pub fn looks_like_json_array(input: &str) -> bool {
let trimmed = input.trim();
trimmed.len() >= 2 && trimmed.starts_with('[') && trimmed.ends_with(']')
}
pub fn detect_json_kind(input: &str) -> JsonKind {
let trimmed = input.trim();
if trimmed.is_empty() {
JsonKind::Unknown
} else if looks_like_json_object(trimmed) {
JsonKind::Object
} else if looks_like_json_array(trimmed) {
JsonKind::Array
} else if is_json_null(trimmed) {
JsonKind::Null
} else if is_json_bool(trimmed) {
JsonKind::Bool
} else if is_json_string(trimmed) {
JsonKind::String
} else if is_json_number(trimmed) {
JsonKind::Number
} else {
JsonKind::Unknown
}
}
pub fn is_json_null(input: &str) -> bool {
input.trim() == "null"
}
pub fn is_json_bool(input: &str) -> bool {
matches!(input.trim(), "true" | "false")
}
pub fn is_json_string(input: &str) -> bool {
unquote_json_string(input).is_some()
}
pub fn is_json_number(input: &str) -> bool {
let bytes = input.trim().as_bytes();
if bytes.is_empty() {
return false;
}
let mut index = 0;
if bytes[index] == b'-' {
index += 1;
}
if index >= bytes.len() {
return false;
}
if bytes[index] == b'0' {
index += 1;
} else if bytes[index].is_ascii_digit() {
while index < bytes.len() && bytes[index].is_ascii_digit() {
index += 1;
}
} else {
return false;
}
if index < bytes.len() && bytes[index].is_ascii_digit() && bytes[index - 1] == b'0' {
return false;
}
if index < bytes.len() && bytes[index] == b'.' {
index += 1;
let fraction_start = index;
while index < bytes.len() && bytes[index].is_ascii_digit() {
index += 1;
}
if fraction_start == index {
return false;
}
}
if index < bytes.len() && matches!(bytes[index], b'e' | b'E') {
index += 1;
if index < bytes.len() && matches!(bytes[index], b'+' | b'-') {
index += 1;
}
let exponent_start = index;
while index < bytes.len() && bytes[index].is_ascii_digit() {
index += 1;
}
if exponent_start == index {
return false;
}
}
index == bytes.len()
}
pub fn quote_json_string(input: &str) -> String {
format!("\"{}\"", escape_json_string(input))
}
pub fn unquote_json_string(input: &str) -> Option<String> {
let trimmed = input.trim();
if trimmed.len() < 2 || !trimmed.starts_with('"') || !trimmed.ends_with('"') {
return None;
}
let inner = &trimmed[1..trimmed.len() - 1];
let mut chars = inner.chars();
let mut output = String::new();
while let Some(ch) = chars.next() {
if ch == '"' || ch.is_control() {
return None;
}
if ch != '\\' {
output.push(ch);
continue;
}
let escaped = chars.next()?;
match escaped {
'"' => output.push('"'),
'\\' => output.push('\\'),
'/' => output.push('/'),
'b' => output.push('\u{0008}'),
'f' => output.push('\u{000C}'),
'n' => output.push('\n'),
'r' => output.push('\r'),
't' => output.push('\t'),
'u' => {
let mut hex = String::with_capacity(4);
for _ in 0..4 {
hex.push(chars.next()?);
}
let value = u32::from_str_radix(&hex, 16).ok()?;
output.push(char::from_u32(value)?);
}
_ => return None,
}
}
Some(output)
}
pub fn escape_json_string(input: &str) -> String {
let mut escaped = String::with_capacity(input.len());
for ch in input.chars() {
match ch {
'"' => escaped.push_str("\\\""),
'\\' => escaped.push_str("\\\\"),
'\u{0008}' => escaped.push_str("\\b"),
'\u{000C}' => escaped.push_str("\\f"),
'\n' => escaped.push_str("\\n"),
'\r' => escaped.push_str("\\r"),
'\t' => escaped.push_str("\\t"),
ch if ch.is_control() => {
escaped.push_str(&format!("\\u{:04X}", ch as u32));
}
_ => escaped.push(ch),
}
}
escaped
}
pub fn compact_json_basic(input: &str) -> String {
let mut compact = String::with_capacity(input.len());
let mut in_string = false;
let mut escaped = false;
for ch in input.chars() {
if in_string {
compact.push(ch);
if escaped {
escaped = false;
} else if ch == '\\' {
escaped = true;
} else if ch == '"' {
in_string = false;
}
continue;
}
if ch.is_whitespace() {
continue;
}
if ch == '"' {
in_string = true;
}
compact.push(ch);
}
compact
}