use std::path::Path;
use std::sync::{Arc, RwLock};
use super::{FilePattern, SchemaContent, SchemaProvider};
struct ExtensionSchema {
id: String,
name: String,
patterns: Vec<FilePattern>,
schema_json: String,
}
impl SchemaProvider for ExtensionSchema {
fn id(&self) -> &str {
&self.id
}
fn name(&self) -> &str {
&self.name
}
fn schema_for(&self, path: &Path) -> Option<SchemaContent> {
if self.patterns.iter().any(|p| p.matches(path)) {
Some(SchemaContent {
id: self.id.clone(),
name: self.name.clone(),
schema_json: self.schema_json.clone(),
})
} else {
None
}
}
}
pub struct SchemaRegistry {
providers: Vec<Box<dyn SchemaProvider>>,
}
impl SchemaRegistry {
pub fn new() -> Self {
Self {
providers: Vec::new(),
}
}
pub fn shared() -> Arc<RwLock<Self>> {
Arc::new(RwLock::new(Self::new()))
}
pub fn register(&mut self, provider: impl SchemaProvider + 'static) {
self.providers.push(Box::new(provider));
}
pub fn register_from_extension(
&mut self,
id: String,
name: String,
patterns: Vec<String>,
schema_json: String,
) {
let file_patterns = patterns
.into_iter()
.map(|p| {
if p.contains('*') || p.contains('/') {
FilePattern::Glob(p)
} else {
FilePattern::Filename(p)
}
})
.collect();
self.providers.push(Box::new(ExtensionSchema {
id,
name,
patterns: file_patterns,
schema_json,
}));
}
pub fn resolve(&self, path: &Path) -> Option<SchemaContent> {
self.providers.iter().find_map(|p| p.schema_for(path))
}
pub fn has_schema_for(&self, path: &Path) -> bool {
self.providers.iter().any(|p| p.schema_for(path).is_some())
}
pub fn provider_count(&self) -> usize {
self.providers.len()
}
}
impl Default for SchemaRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::{FilePattern, SchemaContent, SchemaProvider};
use std::path::Path;
struct StaticProvider {
id: &'static str,
name: &'static str,
pattern: FilePattern,
schema: &'static str,
}
impl SchemaProvider for StaticProvider {
fn id(&self) -> &str {
self.id
}
fn name(&self) -> &str {
self.name
}
fn schema_for(&self, path: &Path) -> Option<SchemaContent> {
if self.pattern.matches(path) {
Some(SchemaContent {
id: self.id.to_string(),
name: self.name.to_string(),
schema_json: self.schema.to_string(),
})
} else {
None
}
}
}
#[test]
fn empty_registry_resolves_none() {
let reg = SchemaRegistry::new();
assert!(reg.resolve(Path::new("Cargo.toml")).is_none());
assert!(!reg.has_schema_for(Path::new("Cargo.toml")));
assert_eq!(reg.provider_count(), 0);
}
#[test]
fn register_custom_provider_and_resolve() {
let mut reg = SchemaRegistry::new();
reg.register(StaticProvider {
id: "cargo-toml",
name: "Cargo.toml Schema",
pattern: FilePattern::Filename("Cargo.toml".into()),
schema: r#"{"type":"object"}"#,
});
let content = reg.resolve(Path::new("/project/Cargo.toml"));
assert!(content.is_some());
let c = content.unwrap();
assert_eq!(c.id, "cargo-toml");
assert_eq!(c.name, "Cargo.toml Schema");
assert_eq!(c.schema_json, r#"{"type":"object"}"#);
}
#[test]
fn custom_provider_no_match() {
let mut reg = SchemaRegistry::new();
reg.register(StaticProvider {
id: "cargo-toml",
name: "Cargo.toml Schema",
pattern: FilePattern::Filename("Cargo.toml".into()),
schema: "{}",
});
assert!(reg.resolve(Path::new("/project/package.json")).is_none());
assert!(reg.has_schema_for(Path::new("Cargo.toml")));
assert!(!reg.has_schema_for(Path::new("package.json")));
}
#[test]
fn register_from_extension_filename_pattern() {
let mut reg = SchemaRegistry::new();
reg.register_from_extension(
"ext-schema".into(),
"Ext Schema".into(),
vec!["myconfig.yaml".into()],
r#"{"type":"object"}"#.into(),
);
assert!(reg.resolve(Path::new("/project/myconfig.yaml")).is_some());
assert!(reg.resolve(Path::new("myconfig.yaml")).is_some());
assert!(reg.resolve(Path::new("/project/other.yaml")).is_none());
}
#[test]
fn register_from_extension_glob_pattern() {
let mut reg = SchemaRegistry::new();
reg.register_from_extension(
"yaml-schema".into(),
"YAML Schema".into(),
vec!["*.yaml".into(), "*.yml".into()],
r#"{"type":"object"}"#.into(),
);
assert!(reg.resolve(Path::new("config.yaml")).is_some());
assert!(reg.resolve(Path::new("/project/config.yaml")).is_some());
assert!(reg.resolve(Path::new("ci.yml")).is_some());
assert!(reg.resolve(Path::new("config.json")).is_none());
}
#[test]
fn register_from_extension_deep_glob_pattern() {
let mut reg = SchemaRegistry::new();
reg.register_from_extension(
"workflows".into(),
"GitHub Actions".into(),
vec![".github/workflows/*.yml".into()],
"{}".into(),
);
assert!(reg.resolve(Path::new(".github/workflows/ci.yml")).is_some());
assert!(reg
.resolve(Path::new("/project/.github/workflows/release.yml"))
.is_some());
assert!(reg
.resolve(Path::new(".github/workflows/sub/ci.yml"))
.is_none());
}
#[test]
fn first_matching_provider_wins() {
let mut reg = SchemaRegistry::new();
reg.register(StaticProvider {
id: "first",
name: "First",
pattern: FilePattern::Filename("config.yaml".into()),
schema: r#"{"title":"first"}"#,
});
reg.register(StaticProvider {
id: "second",
name: "Second",
pattern: FilePattern::Filename("config.yaml".into()),
schema: r#"{"title":"second"}"#,
});
let c = reg.resolve(Path::new("config.yaml")).unwrap();
assert_eq!(c.id, "first");
}
#[test]
fn provider_count_reflects_registrations() {
let mut reg = SchemaRegistry::new();
assert_eq!(reg.provider_count(), 0);
reg.register_from_extension("a".into(), "A".into(), vec![], "{}".into());
reg.register_from_extension("b".into(), "B".into(), vec![], "{}".into());
assert_eq!(reg.provider_count(), 2);
}
#[test]
fn shared_creates_arc_rwlock() {
let shared = SchemaRegistry::shared();
{
let mut reg = shared.write().unwrap();
reg.register_from_extension(
"test".into(),
"Test".into(),
vec!["test.yaml".into()],
"{}".into(),
);
}
let reg = shared.read().unwrap();
assert!(reg.resolve(Path::new("test.yaml")).is_some());
}
}