distributed_cli 4.0.0

The `distributed` CLI for Distributed applications: contracts check/accept, scaffold projects, describe manifests, compile clients, and render schema artifacts. Also a library so other CLIs (e.g. hops) can mount its commands.
Documentation
use std::collections::BTreeSet;

use base64::Engine as _;
use serde_json::Value as JsonValue;

use crate::client_compiler::manifest::{ManifestCommand, ManifestField};
use crate::client_compiler::ClientCompileError;

pub(super) fn constant_matches(value: &JsonValue, expected: &ManifestField) -> bool {
    if expected.scalar == "JSON" {
        return true;
    }
    match (expected.scalar.as_str(), value) {
        ("Boolean", JsonValue::Bool(_)) => true,
        ("BigInt", JsonValue::Number(number)) => number.is_i64() || number.is_u64(),
        ("Int", JsonValue::Number(number)) => {
            number
                .as_i64()
                .is_some_and(|value| i32::try_from(value).is_ok())
                || number
                    .as_u64()
                    .is_some_and(|value| i32::try_from(value).is_ok())
        }
        ("Float", JsonValue::Number(_)) => true,
        ("String" | "ID", JsonValue::String(_)) => true,
        ("Timestamptz", JsonValue::String(value)) => is_rfc3339(value),
        ("Bytea", JsonValue::String(value)) => base64::engine::general_purpose::STANDARD
            .decode(value)
            .is_ok(),
        _ => false,
    }
}

fn is_rfc3339(value: &str) -> bool {
    let bytes = value.as_bytes();
    if bytes.len() < 20
        || bytes.get(4) != Some(&b'-')
        || bytes.get(7) != Some(&b'-')
        || !matches!(bytes.get(10), Some(b'T' | b't'))
        || bytes.get(13) != Some(&b':')
        || bytes.get(16) != Some(&b':')
    {
        return false;
    }
    let number = |range: std::ops::Range<usize>| {
        std::str::from_utf8(bytes.get(range)?)
            .ok()?
            .parse::<u32>()
            .ok()
    };
    let (Some(year), Some(month), Some(day), Some(hour), Some(minute), Some(second)) = (
        number(0..4),
        number(5..7),
        number(8..10),
        number(11..13),
        number(14..16),
        number(17..19),
    ) else {
        return false;
    };
    let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
    let max_day = match month {
        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
        4 | 6 | 9 | 11 => 30,
        2 if leap => 29,
        2 => 28,
        _ => return false,
    };
    if day == 0 || day > max_day || hour > 23 || minute > 59 || second > 60 {
        return false;
    }
    let mut cursor = 19;
    if bytes.get(cursor) == Some(&b'.') {
        cursor += 1;
        let start = cursor;
        while bytes.get(cursor).is_some_and(u8::is_ascii_digit) {
            cursor += 1;
        }
        if cursor == start {
            return false;
        }
    }
    match bytes.get(cursor) {
        Some(b'Z' | b'z') => cursor + 1 == bytes.len(),
        Some(b'+' | b'-') if cursor + 6 == bytes.len() && bytes.get(cursor + 3) == Some(&b':') => {
            matches!(
                (
                    number(cursor + 1..cursor + 3),
                    number(cursor + 4..cursor + 6)
                ),
                (Some(0..=23), Some(0..=59))
            )
        }
        _ => false,
    }
}

pub(super) fn graphql_name(value: &str, label: &str) -> Result<(), ClientCompileError> {
    if !crate::client_compiler::is_graphql_name(value) || value.starts_with("__") {
        Err(invalid(
            "client.manifest.graphql_name",
            format!("{label} `{value}` must be a valid GraphQL name"),
        ))
    } else {
        Ok(())
    }
}

pub(super) fn unique_nonempty(values: &[String], label: &str) -> Result<(), ClientCompileError> {
    let mut seen = BTreeSet::new();
    for value in values {
        nonempty(value, label)?;
        if !seen.insert(value) {
            return Err(invalid(
                "client.manifest.duplicate_entry",
                format!("{label} entries must be unique"),
            ));
        }
    }
    Ok(())
}

pub(super) fn nonempty(value: &str, label: &str) -> Result<(), ClientCompileError> {
    if value.trim().is_empty() {
        Err(invalid(
            "client.manifest.empty",
            format!("{label} must not be empty"),
        ))
    } else {
        Ok(())
    }
}

pub(super) fn command_error(
    command: &ManifestCommand,
    code: &'static str,
    message: impl std::fmt::Display,
) -> ClientCompileError {
    invalid(
        code,
        format!("manifest command `{}` {message}", command.name),
    )
}

pub(super) fn invalid(code: &'static str, message: impl Into<String>) -> ClientCompileError {
    ClientCompileError::manifest(code, message)
}