pub mod registry;
pub mod deep_completion;
pub mod context;
pub mod format;
pub use deep_completion::{completions_from_schema, completions_from_parsed_schema};
pub use registry::SchemaRegistry;
use std::path::Path;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SchemaContent {
pub id: String,
pub name: String,
pub schema_json: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FilePattern {
Filename(String),
Glob(String),
}
impl FilePattern {
pub fn matches(&self, path: &Path) -> bool {
match self {
FilePattern::Filename(name) => path
.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n == name),
FilePattern::Glob(glob) => {
let path_str = path.to_string_lossy();
let normalised = path_str.replace('\\', "/");
let pat = glob.as_bytes();
if glob_match(pat, normalised.as_bytes()) {
return true;
}
for (i, _) in normalised.match_indices('/') {
if glob_match(pat, &normalised.as_bytes()[i + 1..]) {
return true;
}
}
false
}
}
}
}
pub trait SchemaProvider: Send + Sync {
fn id(&self) -> &str;
fn name(&self) -> &str;
fn schema_for(&self, path: &Path) -> Option<SchemaContent>;
}
fn glob_match(pat: &[u8], text: &[u8]) -> bool {
match pat {
[] => text.is_empty(),
[b'*', b'*', rest @ ..] => {
let rest = rest.strip_prefix(b"/").unwrap_or(rest);
if rest.is_empty() {
return true;
}
if glob_match(rest, text) {
return true;
}
for i in 0..text.len() {
if text[i] == b'/' && glob_match(rest, &text[i + 1..]) {
return true;
}
}
false
}
[b'*', rest @ ..] => {
for i in 0..=text.len() {
if i < text.len() && text[i] == b'/' {
break;
}
if glob_match(rest, &text[i..]) {
return true;
}
}
false
}
[p, rest_p @ ..] => match text {
[t, rest_t @ ..] if p == t => glob_match(rest_p, rest_t),
_ => false,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn p(s: &str) -> PathBuf {
PathBuf::from(s)
}
#[test]
fn filename_matches_exact_name() {
let pat = FilePattern::Filename("Cargo.toml".into());
assert!(pat.matches(&p("Cargo.toml")));
assert!(pat.matches(&p("/project/Cargo.toml")));
assert!(pat.matches(&p("nested/dir/Cargo.toml")));
}
#[test]
fn filename_case_sensitive() {
let pat = FilePattern::Filename("Cargo.toml".into());
assert!(!pat.matches(&p("cargo.toml")));
assert!(!pat.matches(&p("CARGO.TOML")));
}
#[test]
fn filename_no_match_different_file() {
let pat = FilePattern::Filename("Cargo.toml".into());
assert!(!pat.matches(&p("/project/package.json")));
}
#[test]
fn glob_star_matches_extension() {
let pat = FilePattern::Glob("*.yaml".into());
assert!(pat.matches(&p("config.yaml")));
assert!(pat.matches(&p("/project/config.yaml")));
assert!(pat.matches(&p("nested/deep/config.yaml")));
}
#[test]
fn glob_star_does_not_match_different_extension() {
let pat = FilePattern::Glob("*.yaml".into());
assert!(!pat.matches(&p("config.json")));
assert!(!pat.matches(&p("config.yaml.bak")));
}
#[test]
fn glob_double_star_matches_any_depth() {
let pat = FilePattern::Glob("**/*.json".into());
assert!(pat.matches(&p("top.json")));
assert!(pat.matches(&p("/project/a/b/c.json")));
assert!(!pat.matches(&p("/project/a/b/c.yaml")));
}
#[test]
fn glob_path_pattern_workflows() {
let pat = FilePattern::Glob(".github/workflows/*.yml".into());
assert!(pat.matches(&p(".github/workflows/ci.yml")));
assert!(pat.matches(&p("/project/.github/workflows/release.yml")));
assert!(!pat.matches(&p(".github/workflows/sub/ci.yml")));
}
#[test]
fn glob_literal_filename() {
let pat = FilePattern::Glob("pyproject.toml".into());
assert!(pat.matches(&p("pyproject.toml")));
assert!(pat.matches(&p("/project/pyproject.toml")));
assert!(!pat.matches(&p("/project/other.toml")));
}
#[test]
fn glob_match_empty_matches_empty() {
assert!(glob_match(b"", b""));
assert!(!glob_match(b"", b"x"));
}
#[test]
fn glob_match_literal() {
assert!(glob_match(b"hello", b"hello"));
assert!(!glob_match(b"hello", b"world"));
assert!(!glob_match(b"hello", b"hell"));
}
#[test]
fn glob_match_star_within_segment() {
assert!(glob_match(b"*.rs", b"main.rs"));
assert!(glob_match(b"*.rs", b".rs")); assert!(!glob_match(b"*.rs", b"src/main.rs"));
}
#[test]
fn glob_match_double_star() {
assert!(glob_match(b"**/*.rs", b"src/main.rs"));
assert!(glob_match(b"**/*.rs", b"a/b/c/main.rs"));
assert!(glob_match(b"**/*.rs", b"main.rs")); assert!(!glob_match(b"**/*.rs", b"main.py"));
}
#[test]
fn glob_match_double_star_only() {
assert!(glob_match(b"**", b""));
assert!(glob_match(b"**", b"a/b/c"));
assert!(glob_match(b"**", b"anything"));
}
}