use std::path::Path;
use std::sync::Arc;
use memstead_schema::{Schema, loader::SchemaLoadError};
#[derive(Debug, thiserror::Error)]
pub enum MemRepoSchemasError {
#[error("could not open mem-repo gitdir: {0}")]
GixOpen(String),
#[error("git tree read error: {0}")]
GitTree(String),
#[error("schema blob {0} is not valid UTF-8: {1}")]
NotUtf8(String, String),
#[error("schema '{name}': {source}")]
Schema {
name: String,
#[source]
source: SchemaLoadError,
},
}
pub enum LoadOutcome {
NoMemRepo,
NoSchemas,
Schemas(Vec<Arc<Schema>>),
}
pub fn load_schemas_from_ref(workspace_root: &Path) -> Result<LoadOutcome, MemRepoSchemasError> {
crate::storage_memstead::load_schemas_from_memstead_ref(workspace_root)
}
pub struct GitBranchSchemaSource {
workspace_root: std::path::PathBuf,
}
impl GitBranchSchemaSource {
pub fn for_workspace(workspace_root: &Path) -> Self {
Self {
workspace_root: workspace_root.to_path_buf(),
}
}
}
impl memstead_base::schema_source::SchemaSource for GitBranchSchemaSource {
fn read_schemas(
&self,
) -> Result<Vec<Arc<Schema>>, memstead_base::schema_source::SchemaSourceError> {
match load_schemas_from_ref(&self.workspace_root) {
Ok(LoadOutcome::Schemas(schemas)) => Ok(schemas),
Ok(LoadOutcome::NoMemRepo) | Ok(LoadOutcome::NoSchemas) => Ok(Vec::new()),
Err(e) => Err(memstead_base::schema_source::SchemaSourceError::Read(
e.to_string(),
)),
}
}
fn write_schema(
&self,
name: &str,
version: &str,
files: &[(String, Vec<u8>)],
) -> Result<(), memstead_base::schema_source::SchemaSourceError> {
let gitdir = self.workspace_root.join("mem-repo").join(".git");
crate::storage_memstead::write_schema_to_memstead_ref(&gitdir, name, version, files)
.map(|_| ())
.map_err(|e| memstead_base::schema_source::SchemaSourceError::Write(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
type SchemaSeed<'a> = (&'a str, &'a str, &'a [(&'a str, &'a str)]);
fn seed_mem_repo_with_schemas(schemas: &[SchemaSeed<'_>]) -> TempDir {
let tmp = TempDir::new().unwrap();
let gitdir = tmp.path().join("mem-repo").join(".git");
std::fs::create_dir_all(&gitdir).unwrap();
let repo = gix::init_bare(&gitdir).unwrap();
let actor = gix::actor::Signature {
name: "test".into(),
email: "test@example.com".into(),
time: gix::date::Time {
seconds: 0,
offset: 0,
},
};
{
let mut buf = gix::date::parse::TimeBuf::default();
let actor_ref = actor.to_ref(&mut buf);
repo.commit_as(
actor_ref,
actor_ref,
"refs/heads/__SYSTEM",
"seed __SYSTEM",
repo.empty_tree().id().detach(),
Vec::<gix::ObjectId>::new(),
)
.unwrap();
}
let mut editor = repo.empty_tree().edit().unwrap();
for (schema_name, manifest_yaml, type_files) in schemas {
let manifest_blob = repo.write_blob(manifest_yaml.as_bytes()).unwrap().detach();
editor
.upsert(
format!("{schema_name}/schema.yaml"),
gix::objs::tree::EntryKind::Blob,
manifest_blob,
)
.unwrap();
for (stem, contents) in *type_files {
let blob = repo.write_blob(contents.as_bytes()).unwrap().detach();
editor
.upsert(
format!("{schema_name}/types/{stem}.yaml"),
gix::objs::tree::EntryKind::Blob,
blob,
)
.unwrap();
}
}
let tree_id = editor.write().unwrap().detach();
let mut buf = gix::date::parse::TimeBuf::default();
let actor_ref = actor.to_ref(&mut buf);
repo.commit_as(
actor_ref,
actor_ref,
"refs/heads/__SCHEMAS",
"seed __SCHEMAS",
tree_id,
Vec::<gix::ObjectId>::new(),
)
.unwrap();
crate::storage_memstead::migrate_to_memstead_ref(&gitdir).unwrap();
tmp
}
#[test]
fn git_branch_source_round_trips_a_written_package() {
use memstead_base::schema_source::SchemaSource;
let tmp = TempDir::new().unwrap();
let gitdir = tmp.path().join("mem-repo").join(".git");
std::fs::create_dir_all(&gitdir).unwrap();
gix::init_bare(&gitdir).unwrap();
let source = GitBranchSchemaSource::for_workspace(tmp.path());
assert!(source.read_schemas().unwrap().is_empty());
let manifest = br#"name: refsrc
version: 0.1.0
description: A git-branch SchemaSource round-trip fixture.
when_to_use: tests
types:
- doc
relationships:
mode: strict
definitions:
- name: _default
description: fallback
default_weight: 1.0
community:
resolution: 1.0
seed: 42
"#;
let doc = br#"name: doc
description: t
when_to_use: here
sections:
- key: body
heading: Body
required: true
search_weight: 10.0
catch_all: true
write_rules: []
metadata_fields: []
title_weight: 100.0
text_fields:
- body
hierarchy_relationship: _default
propagating_relationships: []
updatable_fields:
- title
- body
health_required_fields:
- body
staleness_threshold_days: 90
write_rules: []
"#;
source
.write_schema(
"refsrc",
"0.1.0",
&[
("schema.yaml".to_string(), manifest.to_vec()),
("types/doc.yaml".to_string(), doc.to_vec()),
],
)
.unwrap();
let schemas = source.read_schemas().unwrap();
assert_eq!(schemas.len(), 1);
assert_eq!(schemas[0].manifest.name, "refsrc");
}
#[test]
fn schema_registry_loads_software_schema_from_schemas_ref() {
let manifest = r#"name: software
version: 1.0.0
description: Minimal software schema for the mem-repo gix-loader test.
when_to_use: In the mem_repo_schemas loader test only.
types:
- sample
relationships:
mode: strict
definitions:
- name: PART_OF
description: Hierarchical containment
default_weight: 3.0
- name: REFERENCES
description: Soft reference
default_weight: 0.5
- name: _default
description: Fallback weight for unknown relationships
default_weight: 1.0
community:
resolution: 1.0
seed: 42
"#;
let sample_type = r#"name: sample
description: Sample type for tests
when_to_use: Whenever a minimal type is needed
sections:
- key: body
heading: Body
required: true
search_weight: 10.0
catch_all: true
write_rules:
- One sentence describing the body.
metadata_fields:
- key: status
description: Lifecycle state
field_type: string
default_value: active
enum_values:
- active
- closed
title_weight: 100.0
text_fields:
- body
hierarchy_relationship: PART_OF
propagating_relationships: []
updatable_fields:
- title
- body
- status
health_required_fields:
- body
staleness_threshold_days: 90
write_rules:
- Keep it short.
"#;
let tmp = seed_mem_repo_with_schemas(&[("software", manifest, &[("sample", sample_type)])]);
let outcome = load_schemas_from_ref(tmp.path()).expect("loader must succeed");
let schemas = match outcome {
LoadOutcome::Schemas(s) => s,
other => panic!(
"expected Schemas outcome, got: {}",
match other {
LoadOutcome::NoMemRepo => "NoMemRepo",
LoadOutcome::NoSchemas => "NoSchemas",
LoadOutcome::Schemas(_) => unreachable!(),
}
),
};
assert_eq!(
schemas.len(),
1,
"expected one schema, got {}",
schemas.len()
);
let schema = &schemas[0];
assert_eq!(schema.manifest.name, "software");
assert_eq!(schema.version, semver::Version::new(1, 0, 0));
}
#[test]
fn no_mem_repo_returns_no_mem_repo() {
let tmp = TempDir::new().unwrap();
let outcome = load_schemas_from_ref(tmp.path()).expect("loader must not error");
assert!(matches!(outcome, LoadOutcome::NoMemRepo));
}
#[test]
fn empty_stub_mem_repo_returns_no_mem_repo() {
let tmp = TempDir::new().unwrap();
let gitdir = tmp.path().join("mem-repo").join(".git");
std::fs::create_dir_all(&gitdir).unwrap();
gix::init_bare(&gitdir).unwrap();
let outcome = load_schemas_from_ref(tmp.path()).expect("loader must not error");
assert!(matches!(outcome, LoadOutcome::NoMemRepo));
}
#[test]
fn mem_repo_without_memstead_ref_returns_no_mem_repo() {
let tmp = TempDir::new().unwrap();
let gitdir = tmp.path().join("mem-repo").join(".git");
std::fs::create_dir_all(&gitdir).unwrap();
let repo = gix::init_bare(&gitdir).unwrap();
let actor = gix::actor::Signature {
name: "test".into(),
email: "test@example.com".into(),
time: gix::date::Time {
seconds: 0,
offset: 0,
},
};
let mut buf = gix::date::parse::TimeBuf::default();
let actor_ref = actor.to_ref(&mut buf);
repo.commit_as(
actor_ref,
actor_ref,
"refs/heads/__SYSTEM",
"seed __SYSTEM",
repo.empty_tree().id().detach(),
Vec::<gix::ObjectId>::new(),
)
.unwrap();
let outcome = load_schemas_from_ref(tmp.path()).expect("loader must not error");
assert!(matches!(outcome, LoadOutcome::NoMemRepo));
}
impl std::fmt::Debug for LoadOutcome {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LoadOutcome::NoMemRepo => f.write_str("NoMemRepo"),
LoadOutcome::NoSchemas => f.write_str("NoSchemas"),
LoadOutcome::Schemas(s) => write!(f, "Schemas({})", s.len()),
}
}
}
}