use super::codec::{decode_rkyv, encode_rkyv, ensure_section_within_cap, validate_sorted_unique};
use crate::graph::SeleneGraph;
use crate::graph_types::GraphTypeDef;
const GTYP_VERSION: u8 = 2;
pub(in crate::core_provider) fn encode_graph_types(
graph: &SeleneGraph,
) -> Result<Vec<u8>, crate::ProviderError> {
let rows = graph
.meta
.bound_type
.as_ref()
.map(|type_def| vec![(0_u32, (**type_def).clone())])
.unwrap_or_default();
let mut payload = Vec::with_capacity(1);
payload.push(GTYP_VERSION);
payload.extend(encode_rkyv(&rows, "CORE/GTYP")?);
ensure_section_within_cap("CORE/GTYP", payload.len())?;
Ok(payload)
}
pub(in crate::core_provider) fn decode_graph_types(
bytes: &[u8],
) -> Result<Vec<(u32, GraphTypeDef)>, crate::ProviderError> {
let Some((&version, rest)) = bytes.split_first() else {
return Err(crate::ProviderError::InvalidPayload {
reason: "CORE/GTYP section is empty".to_owned(),
});
};
if version != GTYP_VERSION {
return Err(crate::ProviderError::InvalidPayload {
reason: format!(
"CORE/GTYP section version {version} is unsupported (expected {GTYP_VERSION})"
),
});
}
let rows: Vec<(u32, GraphTypeDef)> = decode_rkyv(rest, "CORE/GTYP")?;
validate_sorted_unique(&rows, "CORE/GTYP")?;
Ok(rows)
}
#[cfg(test)]
mod tests {
use selene_core::{
ByteStringType, CharacterStringType, DecimalType, GraphId, LabelSet, PropertyValueType,
db_string,
};
use super::*;
use crate::SharedGraph;
use crate::graph_types::{
EdgeEndpointDef, EdgeTypeDef, NodeTypeDef, PropertyTypeDef, RecordFieldType,
RecordFieldTypeDef, RecordFieldTypes, ValidationMode,
};
#[test]
fn encode_graph_types_writes_version_byte() {
let person = db_string("VersionPerson").unwrap();
let graph_type = GraphTypeDef {
name: db_string("version.graph").unwrap(),
node_types: vec![NodeTypeDef {
name: person.clone(),
key_labels: LabelSet::single(person),
properties: Vec::new(),
validation_mode: ValidationMode::Strict,
}],
edge_types: Vec::new(),
};
let graph = SharedGraph::builder(GraphId::new(211))
.bound_to(graph_type)
.unwrap()
.build()
.unwrap()
.read()
.as_ref()
.clone();
let bytes = encode_graph_types(&graph).unwrap();
assert_eq!(bytes.first(), Some(>YP_VERSION));
}
#[test]
fn gtyp_round_trips_oneof_endpoint() {
let person = db_string("V3OneOfPerson").unwrap();
let company = db_string("V3OneOfCompany").unwrap();
let school = db_string("V3OneOfSchool").unwrap();
let affiliated = db_string("V3_AFFILIATED").unwrap();
let graph_type = GraphTypeDef {
name: db_string("v3.oneof.graph").unwrap(),
node_types: vec![
NodeTypeDef {
name: person.clone(),
key_labels: LabelSet::single(person),
properties: Vec::new(),
validation_mode: ValidationMode::Strict,
},
NodeTypeDef {
name: company.clone(),
key_labels: LabelSet::single(company),
properties: Vec::new(),
validation_mode: ValidationMode::Strict,
},
NodeTypeDef {
name: school.clone(),
key_labels: LabelSet::single(school),
properties: Vec::new(),
validation_mode: ValidationMode::Strict,
},
],
edge_types: vec![EdgeTypeDef {
name: affiliated.clone(),
label: affiliated,
source_node_type: EdgeEndpointDef::NodeType(0),
target_node_type: EdgeEndpointDef::one_of([1, 2]),
properties: Vec::new(),
validation_mode: ValidationMode::Strict,
}],
};
let graph = SharedGraph::builder(GraphId::new(212))
.bound_to(graph_type.clone())
.unwrap()
.build()
.unwrap()
.read()
.as_ref()
.clone();
let bytes = encode_graph_types(&graph).unwrap();
assert_eq!(bytes.first(), Some(>YP_VERSION));
let decoded = decode_graph_types(&bytes).unwrap();
assert_eq!(decoded.len(), 1);
let decoded_graph_type = &decoded[0].1;
decoded_graph_type.validate_ref().unwrap();
let edge_type = &decoded_graph_type.edge_types[0];
assert_eq!(
edge_type.target_node_type,
EdgeEndpointDef::OneOf(vec![1, 2])
);
assert_eq!(edge_type.source_node_type, EdgeEndpointDef::NodeType(0));
}
#[test]
fn gtyp_round_trips_record_field_types() {
let person = db_string("RecordPerson").unwrap();
let config = db_string("config").unwrap();
let host = db_string("host").unwrap();
let ports = db_string("ports").unwrap();
let nested = db_string("nested").unwrap();
let open_nested = db_string("open_nested").unwrap();
let flag = db_string("flag").unwrap();
let amount = db_string("amount").unwrap();
let title = db_string("title").unwrap();
let digest = db_string("digest").unwrap();
let code = db_string("code").unwrap();
let history = db_string("history").unwrap();
let payloads = db_string("payloads").unwrap();
let aliases = db_string("aliases").unwrap();
let decimal = DecimalType::new(5, 2).unwrap();
let history_decimal = DecimalType::new(4, 1).unwrap();
let nested_decimal = DecimalType::new(6, 3).unwrap();
let character_string = CharacterStringType::new(2, 4).unwrap();
let history_character_string = CharacterStringType::new(1, 2).unwrap();
let nested_character_string = CharacterStringType::new(4, 4).unwrap();
let byte_string = ByteStringType::new(2, 4).unwrap();
let history_byte_string = ByteStringType::new(1, 2).unwrap();
let nested_byte_string = ByteStringType::new(4, 4).unwrap();
let record_field_types = RecordFieldTypes(vec![
RecordFieldTypeDef {
name: host,
field_type: RecordFieldType::Scalar(PropertyValueType::String),
required: true,
},
RecordFieldTypeDef {
name: amount.clone(),
field_type: RecordFieldType::Decimal(nested_decimal),
required: false,
},
RecordFieldTypeDef {
name: code.clone(),
field_type: RecordFieldType::CharacterString(nested_character_string),
required: false,
},
RecordFieldTypeDef {
name: digest.clone(),
field_type: RecordFieldType::ByteString(nested_byte_string),
required: false,
},
RecordFieldTypeDef {
name: ports,
field_type: RecordFieldType::List(Box::new(RecordFieldType::NotNull(Box::new(
RecordFieldType::Scalar(PropertyValueType::Int),
)))),
required: false,
},
RecordFieldTypeDef {
name: open_nested,
field_type: RecordFieldType::OpenRecord,
required: false,
},
RecordFieldTypeDef {
name: nested,
field_type: RecordFieldType::Record(Box::new(RecordFieldTypes(vec![
RecordFieldTypeDef {
name: flag,
field_type: RecordFieldType::Scalar(PropertyValueType::Bool),
required: true,
},
]))),
required: false,
},
]);
let graph_type = GraphTypeDef {
name: db_string("record.graph").unwrap(),
node_types: vec![NodeTypeDef {
name: person.clone(),
key_labels: LabelSet::single(person),
properties: vec![
PropertyTypeDef {
name: config,
value_type: PropertyValueType::RecordTyped,
list_element_type: None,
required: false,
default: None,
immutable: false,
unique: false,
decimal_type: None,
character_string_type: None,
byte_string_type: None,
record_field_types: Some(record_field_types.clone()),
},
PropertyTypeDef {
name: amount.clone(),
value_type: PropertyValueType::Decimal,
list_element_type: None,
required: false,
default: None,
immutable: false,
unique: false,
decimal_type: Some(decimal),
character_string_type: None,
byte_string_type: None,
record_field_types: None,
},
PropertyTypeDef {
name: title,
value_type: PropertyValueType::String,
list_element_type: None,
required: false,
default: None,
immutable: false,
unique: false,
decimal_type: None,
character_string_type: Some(character_string),
byte_string_type: None,
record_field_types: None,
},
PropertyTypeDef {
name: digest,
value_type: PropertyValueType::Bytes,
list_element_type: None,
required: false,
default: None,
immutable: false,
unique: false,
decimal_type: None,
character_string_type: None,
byte_string_type: Some(byte_string),
record_field_types: None,
},
PropertyTypeDef {
name: history,
value_type: PropertyValueType::List,
list_element_type: Some(crate::graph_types::PropertyElementType::Decimal(
history_decimal,
)),
required: false,
default: None,
immutable: false,
unique: false,
decimal_type: None,
character_string_type: None,
byte_string_type: None,
record_field_types: None,
},
PropertyTypeDef {
name: payloads,
value_type: PropertyValueType::List,
list_element_type: Some(
crate::graph_types::PropertyElementType::ByteString(
history_byte_string,
),
),
required: false,
default: None,
immutable: false,
unique: false,
decimal_type: None,
character_string_type: None,
byte_string_type: None,
record_field_types: None,
},
PropertyTypeDef {
name: aliases,
value_type: PropertyValueType::List,
list_element_type: Some(
crate::graph_types::PropertyElementType::CharacterString(
history_character_string,
),
),
required: false,
default: None,
immutable: false,
unique: false,
decimal_type: None,
character_string_type: None,
byte_string_type: None,
record_field_types: None,
},
],
validation_mode: ValidationMode::Strict,
}],
edge_types: Vec::new(),
};
let graph = SharedGraph::builder(GraphId::new(213))
.bound_to(graph_type)
.unwrap()
.build()
.unwrap()
.read()
.as_ref()
.clone();
let bytes = encode_graph_types(&graph).unwrap();
assert_eq!(bytes.first(), Some(>YP_VERSION));
let decoded = decode_graph_types(&bytes).unwrap();
assert_eq!(decoded.len(), 1);
let decoded_graph_type = &decoded[0].1;
decoded_graph_type.validate_ref().unwrap();
let property = &decoded_graph_type.node_types[0].properties[0];
assert_eq!(property.value_type, PropertyValueType::RecordTyped);
assert_eq!(property.record_field_types, Some(record_field_types));
let property = &decoded_graph_type.node_types[0].properties[1];
assert_eq!(property.value_type, PropertyValueType::Decimal);
assert_eq!(property.decimal_type, Some(decimal));
let property = &decoded_graph_type.node_types[0].properties[2];
assert_eq!(property.value_type, PropertyValueType::String);
assert_eq!(property.character_string_type, Some(character_string));
let property = &decoded_graph_type.node_types[0].properties[3];
assert_eq!(property.value_type, PropertyValueType::Bytes);
assert_eq!(property.byte_string_type, Some(byte_string));
let property = &decoded_graph_type.node_types[0].properties[4];
assert_eq!(
property.list_element_type,
Some(crate::graph_types::PropertyElementType::Decimal(
history_decimal
))
);
let property = &decoded_graph_type.node_types[0].properties[5];
assert_eq!(
property.list_element_type,
Some(crate::graph_types::PropertyElementType::ByteString(
history_byte_string
))
);
let property = &decoded_graph_type.node_types[0].properties[6];
assert_eq!(
property.list_element_type,
Some(crate::graph_types::PropertyElementType::CharacterString(
history_character_string
))
);
}
#[test]
fn decode_rejects_unknown_or_empty_version() {
let person = db_string("UnknownVersionPerson").unwrap();
let graph_type = GraphTypeDef {
name: db_string("unknown.version.graph").unwrap(),
node_types: vec![NodeTypeDef {
name: person.clone(),
key_labels: LabelSet::single(person),
properties: Vec::new(),
validation_mode: ValidationMode::Strict,
}],
edge_types: Vec::new(),
};
let graph = SharedGraph::builder(GraphId::new(214))
.bound_to(graph_type)
.unwrap()
.build()
.unwrap()
.read()
.as_ref()
.clone();
let mut bytes = encode_graph_types(&graph).unwrap();
bytes[0] = 0xB7; assert!(matches!(
decode_graph_types(&bytes),
Err(crate::ProviderError::InvalidPayload { .. })
));
assert!(matches!(
decode_graph_types(&[]),
Err(crate::ProviderError::InvalidPayload { .. })
));
}
#[test]
fn decode_rejects_pre_descriptor_version_one() {
let person = db_string("PreDescriptorPerson").unwrap();
let graph_type = GraphTypeDef {
name: db_string("pre.descriptor.graph").unwrap(),
node_types: vec![NodeTypeDef {
name: person.clone(),
key_labels: LabelSet::single(person),
properties: Vec::new(),
validation_mode: ValidationMode::Strict,
}],
edge_types: Vec::new(),
};
let graph = SharedGraph::builder(GraphId::new(215))
.bound_to(graph_type)
.unwrap()
.build()
.unwrap()
.read()
.as_ref()
.clone();
let mut bytes = encode_graph_types(&graph).unwrap();
bytes[0] = 1; assert!(matches!(
decode_graph_types(&bytes),
Err(crate::ProviderError::InvalidPayload { reason })
if reason.contains("version 1 is unsupported")
));
}
}