use crate::error::JacsError;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateOptions {
pub jacs_type: String,
pub visibility: DocumentVisibility,
pub custom_schema: Option<String>,
}
impl Default for CreateOptions {
fn default() -> Self {
Self {
jacs_type: "artifact".to_string(),
visibility: DocumentVisibility::Private,
custom_schema: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateOptions {
pub custom_schema: Option<String>,
pub visibility: Option<DocumentVisibility>,
}
impl Default for UpdateOptions {
fn default() -> Self {
Self {
custom_schema: None,
visibility: None,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ListFilter {
pub jacs_type: Option<String>,
pub agent_id: Option<String>,
pub visibility: Option<DocumentVisibility>,
pub limit: Option<usize>,
pub offset: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DocumentSummary {
pub key: String,
pub document_id: String,
pub version: String,
pub jacs_type: String,
pub visibility: DocumentVisibility,
pub created_at: String,
pub agent_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DocumentDiff {
pub key_a: String,
pub key_b: String,
pub diff_text: String,
pub additions: usize,
pub deletions: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum DocumentVisibility {
Public,
Private,
Restricted(Vec<String>),
}
impl Default for DocumentVisibility {
fn default() -> Self {
DocumentVisibility::Private
}
}
impl DocumentVisibility {
pub fn restricted(principals: Vec<String>) -> Result<Self, JacsError> {
if principals.is_empty() {
return Err(JacsError::ValidationError(
"Restricted visibility requires at least one principal".to_string(),
));
}
Ok(DocumentVisibility::Restricted(principals))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create_options_default_is_sensible() {
let opts = CreateOptions::default();
assert_eq!(opts.jacs_type, "artifact");
assert_eq!(opts.visibility, DocumentVisibility::Private);
assert!(opts.custom_schema.is_none());
}
#[test]
fn update_options_default_is_sensible() {
let opts = UpdateOptions::default();
assert!(opts.custom_schema.is_none());
assert!(opts.visibility.is_none());
}
#[test]
fn list_filter_default_has_all_none() {
let filter = ListFilter::default();
assert!(filter.jacs_type.is_none());
assert!(filter.agent_id.is_none());
assert!(filter.visibility.is_none());
assert!(filter.limit.is_none());
assert!(filter.offset.is_none());
}
#[test]
fn list_filter_supports_jacs_type_agent_id_visibility() {
let filter = ListFilter {
jacs_type: Some("agentstate".to_string()),
agent_id: Some("agent-123".to_string()),
visibility: Some(DocumentVisibility::Public),
limit: Some(50),
offset: Some(10),
};
assert_eq!(filter.jacs_type.as_deref(), Some("agentstate"));
assert_eq!(filter.agent_id.as_deref(), Some("agent-123"));
assert_eq!(filter.visibility, Some(DocumentVisibility::Public));
assert_eq!(filter.limit, Some(50));
assert_eq!(filter.offset, Some(10));
}
#[test]
fn document_visibility_default_is_private() {
assert_eq!(DocumentVisibility::default(), DocumentVisibility::Private);
}
#[test]
fn document_visibility_restricted_holds_principals() {
let vis =
DocumentVisibility::Restricted(vec!["agent-a".to_string(), "agent-b".to_string()]);
if let DocumentVisibility::Restricted(principals) = vis {
assert_eq!(principals.len(), 2);
assert_eq!(principals[0], "agent-a");
} else {
panic!("Expected Restricted variant");
}
}
#[test]
fn document_summary_can_be_constructed() {
let summary = DocumentSummary {
key: "id1:v1".to_string(),
document_id: "id1".to_string(),
version: "v1".to_string(),
jacs_type: "artifact".to_string(),
visibility: DocumentVisibility::Public,
created_at: "2026-03-12T00:00:00Z".to_string(),
agent_id: "agent-1".to_string(),
};
assert_eq!(summary.key, "id1:v1");
assert_eq!(summary.jacs_type, "artifact");
}
#[test]
fn document_diff_can_be_constructed() {
let diff = DocumentDiff {
key_a: "id1:v1".to_string(),
key_b: "id1:v2".to_string(),
diff_text: "+ added line\n- removed line".to_string(),
additions: 1,
deletions: 1,
};
assert_eq!(diff.additions, 1);
assert_eq!(diff.deletions, 1);
}
#[test]
fn document_visibility_public_serializes_to_public() {
let vis = DocumentVisibility::Public;
let json = serde_json::to_string(&vis).expect("serialize Public");
assert_eq!(json, r#""public""#);
}
#[test]
fn document_visibility_private_serializes_to_private() {
let vis = DocumentVisibility::Private;
let json = serde_json::to_string(&vis).expect("serialize Private");
assert_eq!(json, r#""private""#);
}
#[test]
fn document_visibility_restricted_serializes_as_flat_array() {
let vis =
DocumentVisibility::Restricted(vec!["agent-a".to_string(), "agent-b".to_string()]);
let json = serde_json::to_string(&vis).expect("serialize Restricted");
let parsed: serde_json::Value = serde_json::from_str(&json).expect("parse serialized JSON");
let arr = parsed
.get("restricted")
.expect("should have 'restricted' key")
.as_array()
.expect("restricted value should be array directly");
assert_eq!(arr.len(), 2);
assert_eq!(arr[0].as_str().unwrap(), "agent-a");
assert_eq!(arr[1].as_str().unwrap(), "agent-b");
}
#[test]
fn document_visibility_public_roundtrips() {
let original = DocumentVisibility::Public;
let json = serde_json::to_string(&original).expect("serialize");
let deserialized: DocumentVisibility = serde_json::from_str(&json).expect("deserialize");
assert_eq!(original, deserialized);
}
#[test]
fn document_visibility_private_roundtrips() {
let original = DocumentVisibility::Private;
let json = serde_json::to_string(&original).expect("serialize");
let deserialized: DocumentVisibility = serde_json::from_str(&json).expect("deserialize");
assert_eq!(original, deserialized);
}
#[test]
fn document_visibility_restricted_roundtrips() {
let original = DocumentVisibility::Restricted(vec![
"agent-x".to_string(),
"role:reviewer".to_string(),
]);
let json = serde_json::to_string(&original).expect("serialize");
let deserialized: DocumentVisibility = serde_json::from_str(&json).expect("deserialize");
assert_eq!(original, deserialized);
}
#[test]
fn document_visibility_deserializes_from_string_public() {
let vis: DocumentVisibility =
serde_json::from_str(r#""public""#).expect("deserialize public");
assert_eq!(vis, DocumentVisibility::Public);
}
#[test]
fn document_visibility_deserializes_from_string_private() {
let vis: DocumentVisibility =
serde_json::from_str(r#""private""#).expect("deserialize private");
assert_eq!(vis, DocumentVisibility::Private);
}
#[test]
fn document_visibility_deserializes_from_object_restricted() {
let vis: DocumentVisibility =
serde_json::from_str(r#"{"restricted":["agent-1","agent-2"]}"#)
.expect("deserialize restricted");
if let DocumentVisibility::Restricted(principals) = vis {
assert_eq!(principals, vec!["agent-1", "agent-2"]);
} else {
panic!("Expected Restricted variant");
}
}
fn visibility_schema() -> serde_json::Value {
let schema_str = include_str!("../../schemas/header/v1/header.schema.json");
let schema: serde_json::Value =
serde_json::from_str(schema_str).expect("parse header schema");
let visibility_def = schema["properties"]["jacsVisibility"].clone();
assert!(
!visibility_def.is_null(),
"jacsVisibility must exist in header schema"
);
visibility_def
}
#[test]
fn schema_validates_visibility_public() {
let schema = visibility_schema();
let validator = jsonschema::validator_for(&schema).expect("compile visibility schema");
let value = serde_json::json!("public");
let result = validator.validate(&value);
assert!(
result.is_ok(),
"public visibility should pass schema validation"
);
}
#[test]
fn schema_validates_visibility_private() {
let schema = visibility_schema();
let validator = jsonschema::validator_for(&schema).expect("compile visibility schema");
let value = serde_json::json!("private");
let result = validator.validate(&value);
assert!(
result.is_ok(),
"private visibility should pass schema validation"
);
}
#[test]
fn schema_validates_visibility_restricted() {
let schema = visibility_schema();
let validator = jsonschema::validator_for(&schema).expect("compile visibility schema");
let value = serde_json::json!({"restricted": ["agent-1", "agent-2"]});
let result = validator.validate(&value);
assert!(
result.is_ok(),
"restricted visibility should pass schema validation"
);
}
#[test]
fn schema_validates_rust_serialized_restricted_matches_schema() {
let schema = visibility_schema();
let validator = jsonschema::validator_for(&schema).expect("compile visibility schema");
let vis =
DocumentVisibility::Restricted(vec!["agent-a".to_string(), "agent-b".to_string()]);
let vis_value: serde_json::Value =
serde_json::to_value(&vis).expect("serialize visibility");
let result = validator.validate(&vis_value);
assert!(
result.is_ok(),
"Rust-serialized Restricted visibility must pass schema validation"
);
}
#[test]
fn schema_rejects_empty_restricted_principals() {
let schema = visibility_schema();
let validator = jsonschema::validator_for(&schema).expect("compile visibility schema");
let value = serde_json::json!({"restricted": []});
let result = validator.validate(&value);
assert!(
result.is_err(),
"empty restricted principals should fail schema validation (minItems: 1)"
);
}
#[test]
fn schema_rejects_invalid_visibility_value() {
let schema = visibility_schema();
let validator = jsonschema::validator_for(&schema).expect("compile visibility schema");
let value = serde_json::json!("invalid");
let result = validator.validate(&value);
assert!(
result.is_err(),
"invalid visibility string should fail schema validation"
);
}
#[test]
fn restricted_constructor_rejects_empty_principals() {
let result = DocumentVisibility::restricted(vec![]);
assert!(result.is_err(), "empty principals should be rejected");
let err = result.unwrap_err();
let err_msg = format!("{}", err);
assert!(
err_msg.contains("at least one principal"),
"Error should mention principals requirement, got: {}",
err_msg
);
}
#[test]
fn restricted_constructor_accepts_non_empty_principals() {
let result = DocumentVisibility::restricted(vec!["agent-1".to_string()]);
assert!(result.is_ok(), "non-empty principals should be accepted");
let vis = result.unwrap();
assert_eq!(
vis,
DocumentVisibility::Restricted(vec!["agent-1".to_string()])
);
}
#[test]
fn restricted_constructor_accepts_multiple_principals() {
let result = DocumentVisibility::restricted(vec![
"agent-1".to_string(),
"role:reviewer".to_string(),
]);
assert!(result.is_ok());
if let DocumentVisibility::Restricted(principals) = result.unwrap() {
assert_eq!(principals.len(), 2);
} else {
panic!("Expected Restricted variant");
}
}
#[test]
fn document_visibility_implements_eq() {
fn assert_eq_impl<T: Eq>() {}
assert_eq_impl::<DocumentVisibility>();
}
}