use crate::codec::primitives::{Reader, Writer};
use crate::codec::value::{decode_position, decode_property_value, validate_position};
use crate::error::{DecodeError, EncodeError};
use crate::limits::MAX_VALUES_PER_ENTITY;
use crate::model::{
CreateEntity, CreateRelation, CreateValueRef, DataType, DeleteEntity, DeleteRelation,
DictionaryBuilder, Op, PropertyValue, RestoreEntity, RestoreRelation,
UnsetLanguage, UnsetValue, UnsetRelationField, UpdateEntity, UpdateRelation, WireDictionaries,
};
const OP_CREATE_ENTITY: u8 = 1;
const OP_UPDATE_ENTITY: u8 = 2;
const OP_DELETE_ENTITY: u8 = 3;
const OP_RESTORE_ENTITY: u8 = 4;
const OP_CREATE_RELATION: u8 = 5;
const OP_UPDATE_RELATION: u8 = 6;
const OP_DELETE_RELATION: u8 = 7;
const OP_RESTORE_RELATION: u8 = 8;
const OP_CREATE_VALUE_REF: u8 = 9;
const FLAG_HAS_SET_PROPERTIES: u8 = 0x01;
const FLAG_HAS_UNSET_VALUES: u8 = 0x02;
const UPDATE_ENTITY_RESERVED_MASK: u8 = 0xFC;
const FLAG_HAS_FROM_SPACE: u8 = 0x01;
const FLAG_HAS_FROM_VERSION: u8 = 0x02;
const FLAG_HAS_TO_SPACE: u8 = 0x04;
const FLAG_HAS_TO_VERSION: u8 = 0x08;
const FLAG_HAS_ENTITY: u8 = 0x10;
const FLAG_HAS_POSITION: u8 = 0x20;
const FLAG_FROM_IS_VALUE_REF: u8 = 0x40;
const FLAG_TO_IS_VALUE_REF: u8 = 0x80;
const FLAG_HAS_LANGUAGE: u8 = 0x01;
const FLAG_HAS_SPACE: u8 = 0x02;
const CREATE_VALUE_REF_RESERVED_MASK: u8 = 0xFC;
const NO_CONTEXT_REF: u32 = 0xFFFFFFFF;
const UPDATE_SET_FROM_SPACE: u8 = 0x01;
const UPDATE_SET_FROM_VERSION: u8 = 0x02;
const UPDATE_SET_TO_SPACE: u8 = 0x04;
const UPDATE_SET_TO_VERSION: u8 = 0x08;
const UPDATE_SET_POSITION: u8 = 0x10;
const UPDATE_SET_RESERVED_MASK: u8 = 0xE0;
const UPDATE_UNSET_FROM_SPACE: u8 = 0x01;
const UPDATE_UNSET_FROM_VERSION: u8 = 0x02;
const UPDATE_UNSET_TO_SPACE: u8 = 0x04;
const UPDATE_UNSET_TO_VERSION: u8 = 0x08;
const UPDATE_UNSET_POSITION: u8 = 0x10;
const UPDATE_UNSET_RESERVED_MASK: u8 = 0xE0;
pub fn decode_op<'a>(reader: &mut Reader<'a>, dicts: &WireDictionaries) -> Result<Op<'a>, DecodeError> {
let op_type = reader.read_byte("op_type")?;
match op_type {
OP_CREATE_ENTITY => decode_create_entity(reader, dicts),
OP_UPDATE_ENTITY => decode_update_entity(reader, dicts),
OP_DELETE_ENTITY => decode_delete_entity(reader, dicts),
OP_RESTORE_ENTITY => decode_restore_entity(reader, dicts),
OP_CREATE_RELATION => decode_create_relation(reader, dicts),
OP_UPDATE_RELATION => decode_update_relation(reader, dicts),
OP_DELETE_RELATION => decode_delete_relation(reader, dicts),
OP_RESTORE_RELATION => decode_restore_relation(reader, dicts),
OP_CREATE_VALUE_REF => decode_create_value_ref(reader, dicts),
_ => Err(DecodeError::InvalidOpType { op_type }),
}
}
fn decode_create_entity<'a>(
reader: &mut Reader<'a>,
dicts: &WireDictionaries,
) -> Result<Op<'a>, DecodeError> {
let id = reader.read_id("entity_id")?;
let value_count = reader.read_varint("value_count")? as usize;
if value_count > MAX_VALUES_PER_ENTITY {
return Err(DecodeError::LengthExceedsLimit {
field: "values",
len: value_count,
max: MAX_VALUES_PER_ENTITY,
});
}
let mut values = Vec::with_capacity(value_count);
for _ in 0..value_count {
values.push(decode_property_value(reader, dicts)?);
}
let context_ref_raw = reader.read_varint("context_ref")? as u32;
let context = if context_ref_raw == NO_CONTEXT_REF {
None
} else {
let idx = context_ref_raw as usize;
Some(dicts.get_context(idx).ok_or_else(|| DecodeError::IndexOutOfBounds {
dict: "contexts",
index: idx,
size: dicts.contexts.len(),
})?.clone())
};
Ok(Op::CreateEntity(CreateEntity { id, values, context }))
}
fn decode_update_entity<'a>(
reader: &mut Reader<'a>,
dicts: &WireDictionaries,
) -> Result<Op<'a>, DecodeError> {
let id_index = reader.read_varint("entity_id")? as usize;
if id_index >= dicts.objects.len() {
return Err(DecodeError::IndexOutOfBounds {
dict: "objects",
index: id_index,
size: dicts.objects.len(),
});
}
let id = dicts.objects[id_index];
let flags = reader.read_byte("update_flags")?;
if flags & UPDATE_ENTITY_RESERVED_MASK != 0 {
return Err(DecodeError::ReservedBitsSet {
context: "UpdateEntity flags",
});
}
let mut update = UpdateEntity::new(id);
if flags & FLAG_HAS_SET_PROPERTIES != 0 {
let count = reader.read_varint("set_properties_count")? as usize;
if count > MAX_VALUES_PER_ENTITY {
return Err(DecodeError::LengthExceedsLimit {
field: "set_properties",
len: count,
max: MAX_VALUES_PER_ENTITY,
});
}
for _ in 0..count {
update.set_properties.push(decode_property_value(reader, dicts)?);
}
}
if flags & FLAG_HAS_UNSET_VALUES != 0 {
let count = reader.read_varint("unset_values_count")? as usize;
if count > MAX_VALUES_PER_ENTITY {
return Err(DecodeError::LengthExceedsLimit {
field: "unset_values",
len: count,
max: MAX_VALUES_PER_ENTITY,
});
}
for _ in 0..count {
let prop_index = reader.read_varint("property")? as usize;
if prop_index >= dicts.properties.len() {
return Err(DecodeError::IndexOutOfBounds {
dict: "properties",
index: prop_index,
size: dicts.properties.len(),
});
}
let property = dicts.properties[prop_index].0;
let lang_value = reader.read_varint("unset.language")? as u32;
let language = if lang_value == 0xFFFFFFFF {
UnsetLanguage::All
} else if lang_value == 0 {
UnsetLanguage::English
} else {
let idx = (lang_value - 1) as usize;
if idx >= dicts.languages.len() {
return Err(DecodeError::IndexOutOfBounds {
dict: "languages",
index: lang_value as usize,
size: dicts.languages.len() + 1,
});
}
UnsetLanguage::Specific(dicts.languages[idx])
};
update.unset_values.push(UnsetValue { property, language });
}
}
let context_ref_raw = reader.read_varint("context_ref")? as u32;
update.context = if context_ref_raw == NO_CONTEXT_REF {
None
} else {
let idx = context_ref_raw as usize;
Some(dicts.get_context(idx).ok_or_else(|| DecodeError::IndexOutOfBounds {
dict: "contexts",
index: idx,
size: dicts.contexts.len(),
})?.clone())
};
Ok(Op::UpdateEntity(update))
}
fn decode_delete_entity<'a>(
reader: &mut Reader<'a>,
dicts: &WireDictionaries,
) -> Result<Op<'a>, DecodeError> {
let id_index = reader.read_varint("entity_id")? as usize;
if id_index >= dicts.objects.len() {
return Err(DecodeError::IndexOutOfBounds {
dict: "objects",
index: id_index,
size: dicts.objects.len(),
});
}
let id = dicts.objects[id_index];
let context_ref_raw = reader.read_varint("context_ref")? as u32;
let context = if context_ref_raw == NO_CONTEXT_REF {
None
} else {
let idx = context_ref_raw as usize;
Some(dicts.get_context(idx).ok_or_else(|| DecodeError::IndexOutOfBounds {
dict: "contexts",
index: idx,
size: dicts.contexts.len(),
})?.clone())
};
Ok(Op::DeleteEntity(DeleteEntity { id, context }))
}
fn decode_restore_entity<'a>(
reader: &mut Reader<'a>,
dicts: &WireDictionaries,
) -> Result<Op<'a>, DecodeError> {
let id_index = reader.read_varint("entity_id")? as usize;
if id_index >= dicts.objects.len() {
return Err(DecodeError::IndexOutOfBounds {
dict: "objects",
index: id_index,
size: dicts.objects.len(),
});
}
let id = dicts.objects[id_index];
let context_ref_raw = reader.read_varint("context_ref")? as u32;
let context = if context_ref_raw == NO_CONTEXT_REF {
None
} else {
let idx = context_ref_raw as usize;
Some(dicts.get_context(idx).ok_or_else(|| DecodeError::IndexOutOfBounds {
dict: "contexts",
index: idx,
size: dicts.contexts.len(),
})?.clone())
};
Ok(Op::RestoreEntity(RestoreEntity { id, context }))
}
fn decode_create_relation<'a>(
reader: &mut Reader<'a>,
dicts: &WireDictionaries,
) -> Result<Op<'a>, DecodeError> {
let id = reader.read_id("relation_id")?;
let type_index = reader.read_varint("relation_type")? as usize;
if type_index >= dicts.relation_types.len() {
return Err(DecodeError::IndexOutOfBounds {
dict: "relation_types",
index: type_index,
size: dicts.relation_types.len(),
});
}
let relation_type = dicts.relation_types[type_index];
let flags = reader.read_byte("relation_flags")?;
let from_is_value_ref = flags & FLAG_FROM_IS_VALUE_REF != 0;
let to_is_value_ref = flags & FLAG_TO_IS_VALUE_REF != 0;
let from = if from_is_value_ref {
reader.read_id("from")?
} else {
let from_index = reader.read_varint("from")? as usize;
if from_index >= dicts.objects.len() {
return Err(DecodeError::IndexOutOfBounds {
dict: "objects",
index: from_index,
size: dicts.objects.len(),
});
}
dicts.objects[from_index]
};
let to = if to_is_value_ref {
reader.read_id("to")?
} else {
let to_index = reader.read_varint("to")? as usize;
if to_index >= dicts.objects.len() {
return Err(DecodeError::IndexOutOfBounds {
dict: "objects",
index: to_index,
size: dicts.objects.len(),
});
}
dicts.objects[to_index]
};
let from_space = if flags & FLAG_HAS_FROM_SPACE != 0 {
Some(reader.read_id("from_space")?)
} else {
None
};
let from_version = if flags & FLAG_HAS_FROM_VERSION != 0 {
Some(reader.read_id("from_version")?)
} else {
None
};
let to_space = if flags & FLAG_HAS_TO_SPACE != 0 {
Some(reader.read_id("to_space")?)
} else {
None
};
let to_version = if flags & FLAG_HAS_TO_VERSION != 0 {
Some(reader.read_id("to_version")?)
} else {
None
};
let entity = if flags & FLAG_HAS_ENTITY != 0 {
Some(reader.read_id("entity_id")?)
} else {
None
};
let position = if flags & FLAG_HAS_POSITION != 0 {
Some(decode_position(reader)?)
} else {
None
};
let context_ref_raw = reader.read_varint("context_ref")? as u32;
let context = if context_ref_raw == NO_CONTEXT_REF {
None
} else {
let idx = context_ref_raw as usize;
Some(dicts.get_context(idx).ok_or_else(|| DecodeError::IndexOutOfBounds {
dict: "contexts",
index: idx,
size: dicts.contexts.len(),
})?.clone())
};
Ok(Op::CreateRelation(CreateRelation {
id,
relation_type,
from,
from_is_value_ref,
to,
to_is_value_ref,
entity,
position,
from_space,
from_version,
to_space,
to_version,
context,
}))
}
fn decode_update_relation<'a>(
reader: &mut Reader<'a>,
dicts: &WireDictionaries,
) -> Result<Op<'a>, DecodeError> {
let id_index = reader.read_varint("relation_id")? as usize;
if id_index >= dicts.objects.len() {
return Err(DecodeError::IndexOutOfBounds {
dict: "objects",
index: id_index,
size: dicts.objects.len(),
});
}
let id = dicts.objects[id_index];
let set_flags = reader.read_byte("set_flags")?;
let unset_flags = reader.read_byte("unset_flags")?;
if set_flags & UPDATE_SET_RESERVED_MASK != 0 {
return Err(DecodeError::ReservedBitsSet {
context: "UpdateRelation set_flags",
});
}
if unset_flags & UPDATE_UNSET_RESERVED_MASK != 0 {
return Err(DecodeError::ReservedBitsSet {
context: "UpdateRelation unset_flags",
});
}
let from_space = if set_flags & UPDATE_SET_FROM_SPACE != 0 {
Some(reader.read_id("from_space")?)
} else {
None
};
let from_version = if set_flags & UPDATE_SET_FROM_VERSION != 0 {
Some(reader.read_id("from_version")?)
} else {
None
};
let to_space = if set_flags & UPDATE_SET_TO_SPACE != 0 {
Some(reader.read_id("to_space")?)
} else {
None
};
let to_version = if set_flags & UPDATE_SET_TO_VERSION != 0 {
Some(reader.read_id("to_version")?)
} else {
None
};
let position = if set_flags & UPDATE_SET_POSITION != 0 {
Some(decode_position(reader)?)
} else {
None
};
let mut unset = Vec::new();
if unset_flags & UPDATE_UNSET_FROM_SPACE != 0 {
unset.push(UnsetRelationField::FromSpace);
}
if unset_flags & UPDATE_UNSET_FROM_VERSION != 0 {
unset.push(UnsetRelationField::FromVersion);
}
if unset_flags & UPDATE_UNSET_TO_SPACE != 0 {
unset.push(UnsetRelationField::ToSpace);
}
if unset_flags & UPDATE_UNSET_TO_VERSION != 0 {
unset.push(UnsetRelationField::ToVersion);
}
if unset_flags & UPDATE_UNSET_POSITION != 0 {
unset.push(UnsetRelationField::Position);
}
let context_ref_raw = reader.read_varint("context_ref")? as u32;
let context = if context_ref_raw == NO_CONTEXT_REF {
None
} else {
let idx = context_ref_raw as usize;
Some(dicts.get_context(idx).ok_or_else(|| DecodeError::IndexOutOfBounds {
dict: "contexts",
index: idx,
size: dicts.contexts.len(),
})?.clone())
};
Ok(Op::UpdateRelation(UpdateRelation {
id,
from_space,
from_version,
to_space,
to_version,
position,
unset,
context,
}))
}
fn decode_delete_relation<'a>(
reader: &mut Reader<'a>,
dicts: &WireDictionaries,
) -> Result<Op<'a>, DecodeError> {
let id_index = reader.read_varint("relation_id")? as usize;
if id_index >= dicts.objects.len() {
return Err(DecodeError::IndexOutOfBounds {
dict: "objects",
index: id_index,
size: dicts.objects.len(),
});
}
let id = dicts.objects[id_index];
let context_ref_raw = reader.read_varint("context_ref")? as u32;
let context = if context_ref_raw == NO_CONTEXT_REF {
None
} else {
let idx = context_ref_raw as usize;
Some(dicts.get_context(idx).ok_or_else(|| DecodeError::IndexOutOfBounds {
dict: "contexts",
index: idx,
size: dicts.contexts.len(),
})?.clone())
};
Ok(Op::DeleteRelation(DeleteRelation { id, context }))
}
fn decode_restore_relation<'a>(
reader: &mut Reader<'a>,
dicts: &WireDictionaries,
) -> Result<Op<'a>, DecodeError> {
let id_index = reader.read_varint("relation_id")? as usize;
if id_index >= dicts.objects.len() {
return Err(DecodeError::IndexOutOfBounds {
dict: "objects",
index: id_index,
size: dicts.objects.len(),
});
}
let id = dicts.objects[id_index];
let context_ref_raw = reader.read_varint("context_ref")? as u32;
let context = if context_ref_raw == NO_CONTEXT_REF {
None
} else {
let idx = context_ref_raw as usize;
Some(dicts.get_context(idx).ok_or_else(|| DecodeError::IndexOutOfBounds {
dict: "contexts",
index: idx,
size: dicts.contexts.len(),
})?.clone())
};
Ok(Op::RestoreRelation(RestoreRelation { id, context }))
}
fn decode_create_value_ref<'a>(
reader: &mut Reader<'a>,
dicts: &WireDictionaries,
) -> Result<Op<'a>, DecodeError> {
let id = reader.read_id("value_ref_id")?;
let entity_index = reader.read_varint("entity")? as usize;
if entity_index >= dicts.objects.len() {
return Err(DecodeError::IndexOutOfBounds {
dict: "objects",
index: entity_index,
size: dicts.objects.len(),
});
}
let entity = dicts.objects[entity_index];
let property_index = reader.read_varint("property")? as usize;
if property_index >= dicts.properties.len() {
return Err(DecodeError::IndexOutOfBounds {
dict: "properties",
index: property_index,
size: dicts.properties.len(),
});
}
let property = dicts.properties[property_index].0;
let data_type = dicts.properties[property_index].1;
let flags = reader.read_byte("value_ref_flags")?;
if flags & CREATE_VALUE_REF_RESERVED_MASK != 0 {
return Err(DecodeError::ReservedBitsSet {
context: "CreateValueRef flags",
});
}
let language = if flags & FLAG_HAS_LANGUAGE != 0 {
if data_type != DataType::Text {
return Err(DecodeError::MalformedEncoding {
context: "CreateValueRef has_language=1 but property DataType is not TEXT",
});
}
let lang_index = reader.read_varint("language")? as usize;
if lang_index == 0 {
None } else {
let idx = lang_index - 1;
if idx >= dicts.languages.len() {
return Err(DecodeError::IndexOutOfBounds {
dict: "languages",
index: lang_index,
size: dicts.languages.len() + 1,
});
}
Some(dicts.languages[idx])
}
} else {
None
};
let space = if flags & FLAG_HAS_SPACE != 0 {
Some(reader.read_id("space")?)
} else {
None
};
Ok(Op::CreateValueRef(CreateValueRef {
id,
entity,
property,
language,
space,
}))
}
pub fn encode_op(
writer: &mut Writer,
op: &Op<'_>,
dict_builder: &mut DictionaryBuilder,
property_types: &rustc_hash::FxHashMap<crate::model::Id, DataType>,
) -> Result<(), EncodeError> {
match op {
Op::CreateEntity(ce) => encode_create_entity(writer, ce, dict_builder, property_types),
Op::UpdateEntity(ue) => encode_update_entity(writer, ue, dict_builder, property_types),
Op::DeleteEntity(de) => encode_delete_entity(writer, de, dict_builder),
Op::RestoreEntity(re) => encode_restore_entity(writer, re, dict_builder),
Op::CreateRelation(cr) => encode_create_relation(writer, cr, dict_builder),
Op::UpdateRelation(ur) => encode_update_relation(writer, ur, dict_builder),
Op::DeleteRelation(dr) => encode_delete_relation(writer, dr, dict_builder),
Op::RestoreRelation(rr) => encode_restore_relation(writer, rr, dict_builder),
Op::CreateValueRef(cvr) => encode_create_value_ref(writer, cvr, dict_builder),
}
}
fn encode_create_entity(
writer: &mut Writer,
ce: &CreateEntity<'_>,
dict_builder: &mut DictionaryBuilder,
property_types: &rustc_hash::FxHashMap<crate::model::Id, DataType>,
) -> Result<(), EncodeError> {
writer.write_byte(OP_CREATE_ENTITY);
writer.write_id(&ce.id);
writer.write_varint(ce.values.len() as u64);
for pv in &ce.values {
let data_type = property_types.get(&pv.property)
.copied()
.unwrap_or_else(|| pv.value.data_type());
encode_property_value(writer, pv, dict_builder, data_type)?;
}
let context_ref = match &ce.context {
Some(ctx) => dict_builder.add_context(ctx) as u32,
None => NO_CONTEXT_REF,
};
writer.write_varint(context_ref as u64);
Ok(())
}
fn encode_update_entity(
writer: &mut Writer,
ue: &UpdateEntity<'_>,
dict_builder: &mut DictionaryBuilder,
property_types: &rustc_hash::FxHashMap<crate::model::Id, DataType>,
) -> Result<(), EncodeError> {
writer.write_byte(OP_UPDATE_ENTITY);
let id_index = dict_builder.add_object(ue.id);
writer.write_varint(id_index as u64);
let mut flags = 0u8;
if !ue.set_properties.is_empty() {
flags |= FLAG_HAS_SET_PROPERTIES;
}
if !ue.unset_values.is_empty() {
flags |= FLAG_HAS_UNSET_VALUES;
}
writer.write_byte(flags);
if !ue.set_properties.is_empty() {
writer.write_varint(ue.set_properties.len() as u64);
for pv in &ue.set_properties {
let data_type = property_types.get(&pv.property)
.copied()
.unwrap_or_else(|| pv.value.data_type());
encode_property_value(writer, pv, dict_builder, data_type)?;
}
}
if !ue.unset_values.is_empty() {
writer.write_varint(ue.unset_values.len() as u64);
for unset in &ue.unset_values {
let idx = dict_builder.add_property(unset.property, DataType::Boolean);
writer.write_varint(idx as u64);
let lang_value: u32 = match &unset.language {
UnsetLanguage::All => 0xFFFFFFFF,
UnsetLanguage::English => 0,
UnsetLanguage::Specific(lang_id) => {
let lang_index = dict_builder.add_language(Some(*lang_id));
lang_index as u32
}
};
writer.write_varint(lang_value as u64);
}
}
let context_ref = match &ue.context {
Some(ctx) => dict_builder.add_context(ctx) as u32,
None => NO_CONTEXT_REF,
};
writer.write_varint(context_ref as u64);
Ok(())
}
fn encode_delete_entity(
writer: &mut Writer,
de: &DeleteEntity,
dict_builder: &mut DictionaryBuilder,
) -> Result<(), EncodeError> {
writer.write_byte(OP_DELETE_ENTITY);
let id_index = dict_builder.add_object(de.id);
writer.write_varint(id_index as u64);
let context_ref = match &de.context {
Some(ctx) => dict_builder.add_context(ctx) as u32,
None => NO_CONTEXT_REF,
};
writer.write_varint(context_ref as u64);
Ok(())
}
fn encode_restore_entity(
writer: &mut Writer,
re: &RestoreEntity,
dict_builder: &mut DictionaryBuilder,
) -> Result<(), EncodeError> {
writer.write_byte(OP_RESTORE_ENTITY);
let id_index = dict_builder.add_object(re.id);
writer.write_varint(id_index as u64);
let context_ref = match &re.context {
Some(ctx) => dict_builder.add_context(ctx) as u32,
None => NO_CONTEXT_REF,
};
writer.write_varint(context_ref as u64);
Ok(())
}
fn encode_create_relation(
writer: &mut Writer,
cr: &CreateRelation<'_>,
dict_builder: &mut DictionaryBuilder,
) -> Result<(), EncodeError> {
writer.write_byte(OP_CREATE_RELATION);
writer.write_id(&cr.id);
let type_index = dict_builder.add_relation_type(cr.relation_type);
writer.write_varint(type_index as u64);
let mut flags = 0u8;
if cr.from_space.is_some() {
flags |= FLAG_HAS_FROM_SPACE;
}
if cr.from_version.is_some() {
flags |= FLAG_HAS_FROM_VERSION;
}
if cr.to_space.is_some() {
flags |= FLAG_HAS_TO_SPACE;
}
if cr.to_version.is_some() {
flags |= FLAG_HAS_TO_VERSION;
}
if cr.entity.is_some() {
flags |= FLAG_HAS_ENTITY;
}
if cr.position.is_some() {
flags |= FLAG_HAS_POSITION;
}
if cr.from_is_value_ref {
flags |= FLAG_FROM_IS_VALUE_REF;
}
if cr.to_is_value_ref {
flags |= FLAG_TO_IS_VALUE_REF;
}
writer.write_byte(flags);
if cr.from_is_value_ref {
writer.write_id(&cr.from);
} else {
let from_index = dict_builder.add_object(cr.from);
writer.write_varint(from_index as u64);
}
if cr.to_is_value_ref {
writer.write_id(&cr.to);
} else {
let to_index = dict_builder.add_object(cr.to);
writer.write_varint(to_index as u64);
}
if let Some(space) = &cr.from_space {
writer.write_id(space);
}
if let Some(version) = &cr.from_version {
writer.write_id(version);
}
if let Some(space) = &cr.to_space {
writer.write_id(space);
}
if let Some(version) = &cr.to_version {
writer.write_id(version);
}
if let Some(entity) = &cr.entity {
writer.write_id(entity);
}
if let Some(pos) = &cr.position {
validate_position(pos)?;
writer.write_string(pos);
}
let context_ref = match &cr.context {
Some(ctx) => dict_builder.add_context(ctx) as u32,
None => NO_CONTEXT_REF,
};
writer.write_varint(context_ref as u64);
Ok(())
}
fn encode_update_relation(
writer: &mut Writer,
ur: &UpdateRelation<'_>,
dict_builder: &mut DictionaryBuilder,
) -> Result<(), EncodeError> {
writer.write_byte(OP_UPDATE_RELATION);
let id_index = dict_builder.add_object(ur.id);
writer.write_varint(id_index as u64);
let mut set_flags = 0u8;
if ur.from_space.is_some() {
set_flags |= UPDATE_SET_FROM_SPACE;
}
if ur.from_version.is_some() {
set_flags |= UPDATE_SET_FROM_VERSION;
}
if ur.to_space.is_some() {
set_flags |= UPDATE_SET_TO_SPACE;
}
if ur.to_version.is_some() {
set_flags |= UPDATE_SET_TO_VERSION;
}
if ur.position.is_some() {
set_flags |= UPDATE_SET_POSITION;
}
writer.write_byte(set_flags);
let mut unset_flags = 0u8;
for field in &ur.unset {
match field {
UnsetRelationField::FromSpace => unset_flags |= UPDATE_UNSET_FROM_SPACE,
UnsetRelationField::FromVersion => unset_flags |= UPDATE_UNSET_FROM_VERSION,
UnsetRelationField::ToSpace => unset_flags |= UPDATE_UNSET_TO_SPACE,
UnsetRelationField::ToVersion => unset_flags |= UPDATE_UNSET_TO_VERSION,
UnsetRelationField::Position => unset_flags |= UPDATE_UNSET_POSITION,
}
}
writer.write_byte(unset_flags);
if let Some(space) = &ur.from_space {
writer.write_id(space);
}
if let Some(version) = &ur.from_version {
writer.write_id(version);
}
if let Some(space) = &ur.to_space {
writer.write_id(space);
}
if let Some(version) = &ur.to_version {
writer.write_id(version);
}
if let Some(pos) = &ur.position {
validate_position(pos)?;
writer.write_string(pos);
}
let context_ref = match &ur.context {
Some(ctx) => dict_builder.add_context(ctx) as u32,
None => NO_CONTEXT_REF,
};
writer.write_varint(context_ref as u64);
Ok(())
}
fn encode_delete_relation(
writer: &mut Writer,
dr: &DeleteRelation,
dict_builder: &mut DictionaryBuilder,
) -> Result<(), EncodeError> {
writer.write_byte(OP_DELETE_RELATION);
let id_index = dict_builder.add_object(dr.id);
writer.write_varint(id_index as u64);
let context_ref = match &dr.context {
Some(ctx) => dict_builder.add_context(ctx) as u32,
None => NO_CONTEXT_REF,
};
writer.write_varint(context_ref as u64);
Ok(())
}
fn encode_restore_relation(
writer: &mut Writer,
rr: &RestoreRelation,
dict_builder: &mut DictionaryBuilder,
) -> Result<(), EncodeError> {
writer.write_byte(OP_RESTORE_RELATION);
let id_index = dict_builder.add_object(rr.id);
writer.write_varint(id_index as u64);
let context_ref = match &rr.context {
Some(ctx) => dict_builder.add_context(ctx) as u32,
None => NO_CONTEXT_REF,
};
writer.write_varint(context_ref as u64);
Ok(())
}
fn encode_create_value_ref(
writer: &mut Writer,
cvr: &CreateValueRef,
dict_builder: &mut DictionaryBuilder,
) -> Result<(), EncodeError> {
writer.write_byte(OP_CREATE_VALUE_REF);
writer.write_id(&cvr.id);
let entity_index = dict_builder.add_object(cvr.entity);
writer.write_varint(entity_index as u64);
let data_type = if cvr.language.is_some() { DataType::Text } else { DataType::Boolean };
let property_index = dict_builder.add_property(cvr.property, data_type);
writer.write_varint(property_index as u64);
let mut flags = 0u8;
if cvr.language.is_some() {
flags |= FLAG_HAS_LANGUAGE;
}
if cvr.space.is_some() {
flags |= FLAG_HAS_SPACE;
}
writer.write_byte(flags);
if let Some(lang_id) = cvr.language {
let lang_index = dict_builder.add_language(Some(lang_id));
writer.write_varint(lang_index as u64);
}
if let Some(space) = &cvr.space {
writer.write_id(space);
}
Ok(())
}
fn encode_property_value(
writer: &mut Writer,
pv: &PropertyValue<'_>,
dict_builder: &mut DictionaryBuilder,
data_type: DataType,
) -> Result<(), EncodeError> {
let prop_index = dict_builder.add_property(pv.property, data_type);
writer.write_varint(prop_index as u64);
crate::codec::value::encode_value(writer, &pv.value, dict_builder)?;
Ok(())
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use super::*;
use crate::model::Value;
#[test]
fn test_create_entity_roundtrip() {
let op = Op::CreateEntity(CreateEntity {
id: [1u8; 16],
values: vec![PropertyValue {
property: [2u8; 16],
value: Value::Text {
value: Cow::Owned("test".to_string()),
language: None,
},
}],
context: None,
});
let mut dict_builder = DictionaryBuilder::new();
let mut property_types = rustc_hash::FxHashMap::default();
property_types.insert([2u8; 16], DataType::Text);
let mut writer = Writer::new();
encode_op(&mut writer, &op, &mut dict_builder, &property_types).unwrap();
let dicts = dict_builder.build();
let mut reader = Reader::new(writer.as_bytes());
let decoded = decode_op(&mut reader, &dicts).unwrap();
match (&op, &decoded) {
(Op::CreateEntity(e1), Op::CreateEntity(e2)) => {
assert_eq!(e1.id, e2.id);
assert_eq!(e1.values.len(), e2.values.len());
for (v1, v2) in e1.values.iter().zip(e2.values.iter()) {
assert_eq!(v1.property, v2.property);
match (&v1.value, &v2.value) {
(Value::Text { value: s1, language: l1 }, Value::Text { value: s2, language: l2 }) => {
assert_eq!(s1.as_ref(), s2.as_ref());
assert_eq!(l1, l2);
}
_ => panic!("expected Text values"),
}
}
}
_ => panic!("expected CreateEntity"),
}
}
#[test]
fn test_create_relation_roundtrip() {
let op = Op::CreateRelation(CreateRelation {
id: [10u8; 16],
relation_type: [1u8; 16],
from: [2u8; 16],
from_is_value_ref: false,
to: [3u8; 16],
to_is_value_ref: false,
entity: Some([4u8; 16]),
position: Some(Cow::Owned("abc".to_string())),
from_space: None,
from_version: None,
to_space: None,
to_version: None,
context: None,
});
let mut dict_builder = DictionaryBuilder::new();
let property_types = rustc_hash::FxHashMap::default();
let mut writer = Writer::new();
encode_op(&mut writer, &op, &mut dict_builder, &property_types).unwrap();
let dicts = dict_builder.build();
let mut reader = Reader::new(writer.as_bytes());
let decoded = decode_op(&mut reader, &dicts).unwrap();
match (&op, &decoded) {
(Op::CreateRelation(r1), Op::CreateRelation(r2)) => {
assert_eq!(r1.id, r2.id);
assert_eq!(r1.relation_type, r2.relation_type);
assert_eq!(r1.from, r2.from);
assert_eq!(r1.from_is_value_ref, r2.from_is_value_ref);
assert_eq!(r1.to, r2.to);
assert_eq!(r1.to_is_value_ref, r2.to_is_value_ref);
assert_eq!(r1.entity, r2.entity);
match (&r1.position, &r2.position) {
(Some(p1), Some(p2)) => assert_eq!(p1.as_ref(), p2.as_ref()),
(None, None) => {}
_ => panic!("position mismatch"),
}
}
_ => panic!("expected CreateRelation"),
}
}
#[test]
fn test_create_relation_auto_entity_roundtrip() {
let op = Op::CreateRelation(CreateRelation {
id: [10u8; 16],
relation_type: [1u8; 16],
from: [2u8; 16],
from_is_value_ref: false,
to: [3u8; 16],
to_is_value_ref: false,
entity: None,
position: Some(Cow::Owned("abc".to_string())),
from_space: None,
from_version: None,
to_space: None,
to_version: None,
context: None,
});
let mut dict_builder = DictionaryBuilder::new();
let property_types = rustc_hash::FxHashMap::default();
let mut writer = Writer::new();
encode_op(&mut writer, &op, &mut dict_builder, &property_types).unwrap();
let dicts = dict_builder.build();
let mut reader = Reader::new(writer.as_bytes());
let decoded = decode_op(&mut reader, &dicts).unwrap();
match (&op, &decoded) {
(Op::CreateRelation(r1), Op::CreateRelation(r2)) => {
assert_eq!(r1.id, r2.id);
assert_eq!(r1.relation_type, r2.relation_type);
assert_eq!(r1.from, r2.from);
assert_eq!(r1.from_is_value_ref, r2.from_is_value_ref);
assert_eq!(r1.to, r2.to);
assert_eq!(r1.to_is_value_ref, r2.to_is_value_ref);
assert_eq!(r1.entity, r2.entity);
assert!(r1.entity.is_none());
assert!(r2.entity.is_none());
}
_ => panic!("expected CreateRelation"),
}
}
#[test]
fn test_create_relation_with_versions() {
let op = Op::CreateRelation(CreateRelation {
id: [10u8; 16],
relation_type: [1u8; 16],
from: [2u8; 16],
from_is_value_ref: false,
to: [3u8; 16],
to_is_value_ref: false,
entity: Some([4u8; 16]),
position: Some(Cow::Owned("abc".to_string())),
from_space: Some([5u8; 16]),
from_version: Some([6u8; 16]),
to_space: Some([7u8; 16]),
to_version: Some([8u8; 16]),
context: None,
});
let mut dict_builder = DictionaryBuilder::new();
let property_types = rustc_hash::FxHashMap::default();
let mut writer = Writer::new();
encode_op(&mut writer, &op, &mut dict_builder, &property_types).unwrap();
let dicts = dict_builder.build();
let mut reader = Reader::new(writer.as_bytes());
let decoded = decode_op(&mut reader, &dicts).unwrap();
match (&op, &decoded) {
(Op::CreateRelation(r1), Op::CreateRelation(r2)) => {
assert_eq!(r1.id, r2.id);
assert_eq!(r1.relation_type, r2.relation_type);
assert_eq!(r1.from, r2.from);
assert_eq!(r1.from_is_value_ref, r2.from_is_value_ref);
assert_eq!(r1.to, r2.to);
assert_eq!(r1.to_is_value_ref, r2.to_is_value_ref);
assert_eq!(r1.entity, r2.entity);
assert_eq!(r1.from_space, r2.from_space);
assert_eq!(r1.from_version, r2.from_version);
assert_eq!(r1.to_space, r2.to_space);
assert_eq!(r1.to_version, r2.to_version);
}
_ => panic!("expected CreateRelation"),
}
}
#[test]
fn test_create_relation_with_value_ref_endpoint() {
let op = Op::CreateRelation(CreateRelation {
id: [10u8; 16],
relation_type: [1u8; 16],
from: [2u8; 16],
from_is_value_ref: false,
to: [99u8; 16], to_is_value_ref: true,
entity: None,
position: None,
from_space: None,
from_version: None,
to_space: None,
to_version: None,
context: None,
});
let mut dict_builder = DictionaryBuilder::new();
let property_types = rustc_hash::FxHashMap::default();
let mut writer = Writer::new();
encode_op(&mut writer, &op, &mut dict_builder, &property_types).unwrap();
let dicts = dict_builder.build();
let mut reader = Reader::new(writer.as_bytes());
let decoded = decode_op(&mut reader, &dicts).unwrap();
match (&op, &decoded) {
(Op::CreateRelation(r1), Op::CreateRelation(r2)) => {
assert_eq!(r1.id, r2.id);
assert_eq!(r1.from, r2.from);
assert_eq!(r1.from_is_value_ref, r2.from_is_value_ref);
assert!(!r1.from_is_value_ref);
assert_eq!(r1.to, r2.to);
assert_eq!(r1.to_is_value_ref, r2.to_is_value_ref);
assert!(r1.to_is_value_ref);
}
_ => panic!("expected CreateRelation"),
}
}
#[test]
fn test_create_value_ref_roundtrip() {
let op = Op::CreateValueRef(CreateValueRef {
id: [1u8; 16],
entity: [2u8; 16],
property: [3u8; 16],
language: None,
space: None,
});
let mut dict_builder = DictionaryBuilder::new();
let property_types = rustc_hash::FxHashMap::default();
let mut writer = Writer::new();
encode_op(&mut writer, &op, &mut dict_builder, &property_types).unwrap();
let dicts = dict_builder.build();
let mut reader = Reader::new(writer.as_bytes());
let decoded = decode_op(&mut reader, &dicts).unwrap();
match (&op, &decoded) {
(Op::CreateValueRef(v1), Op::CreateValueRef(v2)) => {
assert_eq!(v1.id, v2.id);
assert_eq!(v1.entity, v2.entity);
assert_eq!(v1.property, v2.property);
assert_eq!(v1.language, v2.language);
assert_eq!(v1.space, v2.space);
}
_ => panic!("expected CreateValueRef"),
}
}
#[test]
fn test_create_value_ref_with_language_and_space() {
let op = Op::CreateValueRef(CreateValueRef {
id: [1u8; 16],
entity: [2u8; 16],
property: [3u8; 16],
language: Some([4u8; 16]),
space: Some([5u8; 16]),
});
let mut dict_builder = DictionaryBuilder::new();
let property_types = rustc_hash::FxHashMap::default();
let mut writer = Writer::new();
encode_op(&mut writer, &op, &mut dict_builder, &property_types).unwrap();
let dicts = dict_builder.build();
let mut reader = Reader::new(writer.as_bytes());
let decoded = decode_op(&mut reader, &dicts).unwrap();
match (&op, &decoded) {
(Op::CreateValueRef(v1), Op::CreateValueRef(v2)) => {
assert_eq!(v1.id, v2.id);
assert_eq!(v1.entity, v2.entity);
assert_eq!(v1.property, v2.property);
assert_eq!(v1.language, v2.language);
assert_eq!(v1.space, v2.space);
}
_ => panic!("expected CreateValueRef"),
}
}
#[test]
fn test_update_relation_roundtrip() {
let op = Op::UpdateRelation(UpdateRelation {
id: [1u8; 16],
from_space: Some([2u8; 16]),
from_version: Some([3u8; 16]),
to_space: Some([4u8; 16]),
to_version: Some([5u8; 16]),
position: Some(Cow::Owned("xyz".to_string())),
unset: vec![],
context: None,
});
let mut dict_builder = DictionaryBuilder::new();
dict_builder.add_object([1u8; 16]); let property_types = rustc_hash::FxHashMap::default();
let mut writer = Writer::new();
encode_op(&mut writer, &op, &mut dict_builder, &property_types).unwrap();
let dicts = dict_builder.build();
let mut reader = Reader::new(writer.as_bytes());
let decoded = decode_op(&mut reader, &dicts).unwrap();
match (&op, &decoded) {
(Op::UpdateRelation(r1), Op::UpdateRelation(r2)) => {
assert_eq!(r1.id, r2.id);
assert_eq!(r1.from_space, r2.from_space);
assert_eq!(r1.from_version, r2.from_version);
assert_eq!(r1.to_space, r2.to_space);
assert_eq!(r1.to_version, r2.to_version);
match (&r1.position, &r2.position) {
(Some(p1), Some(p2)) => assert_eq!(p1.as_ref(), p2.as_ref()),
(None, None) => {}
_ => panic!("position mismatch"),
}
assert_eq!(r1.unset, r2.unset);
}
_ => panic!("expected UpdateRelation"),
}
}
#[test]
fn test_update_relation_with_unset() {
let op = Op::UpdateRelation(UpdateRelation {
id: [1u8; 16],
from_space: None,
from_version: None,
to_space: None,
to_version: None,
position: None,
unset: vec![
UnsetRelationField::FromSpace,
UnsetRelationField::ToVersion,
UnsetRelationField::Position,
],
context: None,
});
let mut dict_builder = DictionaryBuilder::new();
dict_builder.add_object([1u8; 16]); let property_types = rustc_hash::FxHashMap::default();
let mut writer = Writer::new();
encode_op(&mut writer, &op, &mut dict_builder, &property_types).unwrap();
let dicts = dict_builder.build();
let mut reader = Reader::new(writer.as_bytes());
let decoded = decode_op(&mut reader, &dicts).unwrap();
match (&op, &decoded) {
(Op::UpdateRelation(r1), Op::UpdateRelation(r2)) => {
assert_eq!(r1.id, r2.id);
assert_eq!(r1.unset.len(), r2.unset.len());
for field in &r1.unset {
assert!(r2.unset.contains(field), "missing unset field: {:?}", field);
}
}
_ => panic!("expected UpdateRelation"),
}
}
}