use-json 0.1.0

Lightweight JSON inspection and formatting helpers for RustUse
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]

/// A conservative classification of JSON input.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JsonKind {
    Null,
    Bool,
    Number,
    String,
    Array,
    Object,
    Unknown,
}

/// Returns `true` when the input looks like a supported JSON value.
pub fn looks_like_json(input: &str) -> bool {
    detect_json_kind(input) != JsonKind::Unknown
}

/// Returns `true` when the input looks like a JSON object.
pub fn looks_like_json_object(input: &str) -> bool {
    let trimmed = input.trim();
    trimmed.len() >= 2 && trimmed.starts_with('{') && trimmed.ends_with('}')
}

/// Returns `true` when the input looks like a JSON array.
pub fn looks_like_json_array(input: &str) -> bool {
    let trimmed = input.trim();
    trimmed.len() >= 2 && trimmed.starts_with('[') && trimmed.ends_with(']')
}

/// Detects the conservative JSON kind represented by the input.
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
    }
}

/// Returns `true` when the input is the `null` literal.
pub fn is_json_null(input: &str) -> bool {
    input.trim() == "null"
}

/// Returns `true` when the input is a JSON boolean literal.
pub fn is_json_bool(input: &str) -> bool {
    matches!(input.trim(), "true" | "false")
}

/// Returns `true` when the input is a quoted JSON string.
pub fn is_json_string(input: &str) -> bool {
    unquote_json_string(input).is_some()
}

/// Returns `true` when the input is a valid JSON number literal.
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()
}

/// Quotes a Rust string as a JSON string literal.
pub fn quote_json_string(input: &str) -> String {
    format!("\"{}\"", escape_json_string(input))
}

/// Unquotes a conservative JSON string literal.
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)
}

/// Escapes a Rust string for JSON string content.
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
}

/// Removes whitespace outside strings without attempting full JSON parsing.
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
}