use base64::prelude::*;
use cedar_policy::Entity;
use cedar_policy::EntityId;
use cedar_policy::EntityTypeName;
use cedar_policy::EntityUid;
use cedar_policy::ExpressionConstructionError;
use cedar_policy::RestrictedExpression;
use serde_json::Value;
use std::collections::HashMap;
use std::collections::HashSet;
use std::str::FromStr;
use std::string::FromUtf8Error;
use std::sync::Arc;
use crate::common::default_entities_limits::{DefaultEntitiesLimits, DefaultEntitiesLimitsError};
use crate::entity_builder::BuildEntityError;
use crate::entity_builder::build_cedar_entity;
use crate::entity_builder::value_to_expr;
const DANGEROUS_PATTERNS: [&str; 6] = [
"<script",
"javascript:",
"data:",
"vbscript:",
"onload=",
"onerror=",
];
#[derive(Debug, Default, Clone, PartialEq)]
pub(crate) struct DefaultEntities {
pub(crate) inner: Arc<HashMap<EntityUid, Entity>>,
}
impl DefaultEntities {
pub(crate) fn get(&self, key: &EntityUid) -> Option<&Entity> {
self.inner.get(key)
}
pub(crate) fn len(&self) -> usize {
self.inner.len()
}
}
#[derive(Debug, Default, Clone, PartialEq)]
pub(crate) struct DefaultEntitiesWithWarns {
inner: DefaultEntities,
warns: Vec<DefaultEntityWarning>,
}
impl DefaultEntitiesWithWarns {
fn new(entities: HashMap<EntityUid, Entity>, warns: Vec<DefaultEntityWarning>) -> Self {
Self {
inner: DefaultEntities {
inner: Arc::new(entities),
},
warns,
}
}
pub(crate) fn entities(&self) -> &DefaultEntities {
&self.inner
}
pub(crate) fn warns(&self) -> &[DefaultEntityWarning] {
&self.warns
}
}
pub(super) fn parse_default_entities_with_warns(
raw_data: Option<HashMap<String, Value>>,
) -> Result<DefaultEntitiesWithWarns, ParseDefaultEntityError> {
let limits = DefaultEntitiesLimits::default();
if let Some(raw_data) = raw_data {
let mut default_entities = HashMap::new();
let mut warns = Vec::new();
for (n, (entry_id, raw_value)) in raw_data.into_iter().enumerate() {
let entity_count = n + 1;
limits
.validate_default_entity(&entry_id, &raw_value)
.map_err(|err| {
ParseEntityErrorKind::LimitsValidation(err).with_entry_id(entry_id.clone())
})?;
limits
.validate_entities_count(entity_count)
.map_err(|err| {
ParseEntityErrorKind::LimitsValidation(err).with_entry_id(entry_id.clone())
})?;
let entity = match &raw_value {
Value::String(b64_string) => {
parse_base64_single_entity(&mut warns, &entry_id, b64_string)?
},
Value::Object(_) => parse_single_entity(&mut warns, &entry_id, &raw_value)?,
_ => {
return Err(ParseEntityErrorKind::IsNotJsonObject.with_entry_id(entry_id));
},
};
default_entities.insert(entity.uid().clone(), entity);
}
Ok(DefaultEntitiesWithWarns::new(default_entities, warns))
} else {
Ok(DefaultEntitiesWithWarns::default())
}
}
#[derive(Debug, thiserror::Error)]
#[error("failed to parse default entity, id: \"{entry_id}\" error: {error}")]
pub(super) struct ParseDefaultEntityError {
pub entry_id: String,
pub error: Box<ParseEntityErrorKind>,
}
#[derive(Debug, thiserror::Error)]
pub(super) enum ParseEntityErrorKind {
#[error("unable to decode base64 string: {0}")]
Base64Decode(#[from] base64::DecodeError),
#[error("unable to decode base64 string as utf8: {0}")]
UnicodeDecode(#[from] FromUtf8Error),
#[error("base64 decoded value is not valid json: {0}")]
Base64DecodedIsNotJson(#[from] serde_json::Error),
#[error("entity ID cannot be empty or whitespace-only")]
EntityIdIsEmpty,
#[error("entity ID contains potentially dangerous content")]
EntityIdIsDangerous,
#[error("entity data must be JSON object")]
IsNotJsonObject,
#[error("entity has invalid 'uid.type' field, expect string")]
InvalidUidTypeField,
#[error("entity has invalid 'entity_type' field, expect string")]
InvalidEntityTypeField,
#[error("entity must have either 'uid.type' or 'entity_type' (legacy format) field")]
HaveNoUidOrEntityTypeField,
#[error(transparent)]
BuildEntity(BuildEntityError),
#[error("Failed to convert attribute '{attr}' to cedar expr: {errs:?}")]
ParseEntityAttribute {
attr: String,
errs: Vec<ExpressionConstructionError>,
},
#[error("default entities limits validation failed: {0}")]
LimitsValidation(#[from] DefaultEntitiesLimitsError),
}
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
pub(crate) enum DefaultEntityWarning {
#[error(
"Could not parse parent UID '{parent_uid_str}' for default entity '{entry_id}': {error}"
)]
InvalidParentUid {
entry_id: String,
parent_uid_str: String,
error: String,
},
#[error(
"In default entity '{entry_id}' each parent entry must be an object with 'type' and 'id'; skipping value: {value}"
)]
NonObjectParentEntry { entry_id: String, value: String },
}
impl ParseEntityErrorKind {
fn with_entry_id(self, entry_id: String) -> ParseDefaultEntityError {
ParseDefaultEntityError {
entry_id,
error: Box::new(self),
}
}
}
fn parse_base64_single_entity(
warns: &mut Vec<DefaultEntityWarning>,
entry_id: &str,
b64: &str,
) -> Result<Entity, ParseDefaultEntityError> {
let buf = BASE64_STANDARD.decode(b64).map_err(|err| {
ParseEntityErrorKind::Base64Decode(err).with_entry_id(entry_id.to_owned())
})?;
let json_str = String::from_utf8(buf).map_err(|err| {
ParseEntityErrorKind::UnicodeDecode(err).with_entry_id(entry_id.to_owned())
})?;
let entity_data: serde_json::Value = serde_json::from_str(&json_str).map_err(|err| {
ParseEntityErrorKind::Base64DecodedIsNotJson(err).with_entry_id(entry_id.to_owned())
})?;
let entity = parse_single_entity(warns, entry_id, &entity_data)?;
Ok(entity)
}
fn parse_single_entity(
warns: &mut Vec<DefaultEntityWarning>,
entry_id: &str,
entity_data: &Value,
) -> Result<Entity, ParseDefaultEntityError> {
validate_entry_id(entry_id)?;
let Value::Object(entity_obj) = entity_data else {
return Err(ParseEntityErrorKind::IsNotJsonObject.with_entry_id(entry_id.to_owned()));
};
let parse_result = if entity_obj.contains_key("uid") {
parse_cedar_format(warns, entry_id, entity_data)?
} else if entity_obj.contains_key("entity_type") {
parse_legacy_format(warns, entry_id, entity_data)?
} else {
return Err(
ParseEntityErrorKind::HaveNoUidOrEntityTypeField.with_entry_id(entry_id.to_owned())
);
};
let entity = build_cedar_entity(
parse_result.entity_type,
parse_result.entity_id,
parse_result.cedar_attrs,
parse_result.parents,
)
.map_err(|err| ParseEntityErrorKind::BuildEntity(err).with_entry_id(entry_id.to_owned()))?;
Ok(entity)
}
fn validate_entry_id(entry_id: &str) -> Result<(), ParseDefaultEntityError> {
if entry_id.trim().is_empty() {
return Err(ParseEntityErrorKind::EntityIdIsEmpty.with_entry_id(entry_id.to_owned()));
}
let entry_id_lower = entry_id.to_lowercase();
for pattern in &DANGEROUS_PATTERNS {
if entry_id_lower.contains(pattern) {
return Err(
ParseEntityErrorKind::EntityIdIsDangerous.with_entry_id(entry_id.to_owned())
);
}
}
Ok(())
}
struct EntityParseResultData<'a> {
pub entity_type: &'a str,
pub entity_id: &'a str,
pub cedar_attrs: HashMap<String, RestrictedExpression>,
pub parents: HashSet<EntityUid>,
}
fn parse_cedar_format<'a>(
warns: &mut Vec<DefaultEntityWarning>,
entry_id: &'a str,
entity_data: &'a Value,
) -> Result<EntityParseResultData<'a>, ParseDefaultEntityError> {
let Value::Object(entity_obj) = entity_data else {
return Err(ParseEntityErrorKind::IsNotJsonObject.with_entry_id(entry_id.to_owned()));
};
let entity_type = entity_obj
.get("uid")
.and_then(|v| v.as_object())
.and_then(|v| v.get("type"))
.and_then(|v| v.as_str())
.ok_or_else(|| {
ParseEntityErrorKind::InvalidUidTypeField.with_entry_id(entry_id.to_owned())
})?;
let entity_id_from_uid = entity_obj
.get("uid")
.and_then(|v| v.as_object())
.and_then(|v| v.get("id"))
.and_then(|v| v.as_str())
.unwrap_or(entry_id);
let empty_map = serde_json::Map::new();
let attrs_obj = entity_obj
.get("attrs")
.and_then(|v| v.as_object())
.unwrap_or(&empty_map);
let cedar_attrs = parse_entity_attrs(attrs_obj.iter(), entry_id)?;
let empty_vec: Vec<Value> = Vec::new();
let parents_array = entity_obj
.get("parents")
.and_then(|v| v.as_array())
.unwrap_or(&empty_vec);
let mut parents_set = HashSet::new();
for parent in parents_array {
if let Value::Object(parent_obj) = parent
&& let (Some(parent_entity_type), Some(id_v)) = (
parent_obj.get("type").and_then(|v| v.as_str()),
parent_obj.get("id").and_then(|v| v.as_str()),
)
{
let entity_id = EntityId::from_str(id_v).unwrap_or_else(|e| match e {});
match EntityTypeName::from_str(parent_entity_type) {
Ok(type_name) => {
let parent_uid = EntityUid::from_type_name_and_id(type_name, entity_id);
parents_set.insert(parent_uid);
},
Err(e) => {
warns.push(DefaultEntityWarning::InvalidParentUid {
entry_id: entry_id.to_string(),
parent_uid_str: format!("{parent_entity_type}::\"{id_v}\""),
error: e.to_string(),
});
},
}
} else {
warns.push(DefaultEntityWarning::NonObjectParentEntry {
entry_id: entry_id.to_string(),
value: parent.to_string(),
});
}
}
Ok(EntityParseResultData {
entity_type,
entity_id: entity_id_from_uid,
cedar_attrs,
parents: parents_set,
})
}
fn parse_legacy_format<'a>(
_warns: &mut Vec<DefaultEntityWarning>,
entry_id: &'a str,
entity_data: &'a Value,
) -> Result<EntityParseResultData<'a>, ParseDefaultEntityError> {
let Value::Object(entity_obj) = entity_data else {
return Err(ParseEntityErrorKind::IsNotJsonObject.with_entry_id(entry_id.to_owned()));
};
let entity_type = entity_obj
.get("entity_type")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ParseEntityErrorKind::InvalidEntityTypeField.with_entry_id(entry_id.to_owned())
})?;
let entity_id_from_uid = entity_obj
.get("entity_id")
.and_then(|v| v.as_str())
.unwrap_or(entry_id);
let cedar_attrs = parse_entity_attrs(
entity_obj
.iter()
.filter(|(key, _)| key != &"entity_type" && key != &"entity_id"),
entry_id,
)?;
Ok(EntityParseResultData {
entity_type,
entity_id: entity_id_from_uid,
cedar_attrs,
parents: HashSet::new(),
})
}
fn parse_entity_attrs<'a>(
attrs_iter: impl Iterator<Item = (&'a String, &'a Value)>,
entry_id: &str,
) -> Result<HashMap<String, RestrictedExpression>, ParseDefaultEntityError> {
let mut cedar_attrs = HashMap::new();
for (key, value) in attrs_iter {
match value_to_expr::value_to_expr(value) {
Ok(Some(expr)) => {
cedar_attrs.insert(key.clone(), expr);
},
Ok(None) => {},
Err(errors) => {
return Err(ParseEntityErrorKind::ParseEntityAttribute {
attr: key.to_owned(),
errs: errors,
}
.with_entry_id(entry_id.to_owned()));
},
}
}
Ok(cedar_attrs)
}
#[cfg(test)]
mod test {
use super::DANGEROUS_PATTERNS;
use super::{DefaultEntityWarning, ParseEntityErrorKind, parse_default_entities_with_warns};
use base64::Engine;
use cedar_policy::EntityUid;
use serde_json::{Value, json};
use std::collections::HashMap;
use std::str::FromStr;
use test_utils::assert_eq;
#[test]
fn can_parse_default_entities() {
let default_entities_data = json!({
"1694c954f8d9".to_string(): json!({
"entity_id": "1694c954f8d9",
"entity_type": "Jans::DefaultEntity",
"o": "Acme Dolphins Division",
"org_id": "100129"
}),
});
let raw_data: HashMap<String, Value> =
serde_json::from_value(default_entities_data).unwrap();
let parsed_entities = parse_default_entities_with_warns(Some(raw_data))
.expect("should parse default entities");
let entities_hashmap = &parsed_entities.entities();
assert_eq!(entities_hashmap.len(), 1, "should have 1 default entity");
let entity = entities_hashmap
.get(&EntityUid::from_str("Jans::DefaultEntity::\"1694c954f8d9\"").unwrap())
.expect("should have entity");
assert_eq!(entity.uid().type_name().to_string(), "Jans::DefaultEntity");
assert_eq!(entity.uid().id().as_ref() as &str, "1694c954f8d9");
}
#[test]
fn test_parse_error_missing_uid() {
let entity_data = json!({
"attrs": {
"attribute": "value"
}
});
let default_entities_data = json!({"test123".to_string(): entity_data});
let raw_data: HashMap<String, Value> =
serde_json::from_value(default_entities_data).unwrap();
let err = parse_default_entities_with_warns(Some(raw_data))
.expect_err("Expected error for entity missing uid field");
assert_eq!(err.entry_id, "test123", "Expected entry_id to be 'test123'");
assert!(
matches!(*err.error, ParseEntityErrorKind::HaveNoUidOrEntityTypeField),
"Expected error to be HaveNoUidOrEntityTypeField"
);
}
#[test]
fn test_parse_error_invalid_uid_structure() {
let entity_data = json!({
"uid": "not-an-object",
"attrs": {}
});
let default_entities_data = json!({"test123".to_string(): entity_data});
let raw_data: HashMap<String, Value> =
serde_json::from_value(default_entities_data).unwrap();
let err = parse_default_entities_with_warns(Some(raw_data))
.expect_err("Expected error when uid is not an object");
assert_eq!(err.entry_id, "test123", "Expected entry_id to be 'test123'");
assert!(
matches!(*err.error, ParseEntityErrorKind::InvalidUidTypeField),
"Expected error to be InvalidUidTypeField"
);
let entity_data_no_type = json!({
"uid": {
"id": "test"
},
"attrs": {}
});
let default_entities_data = json!({"test456".to_string(): entity_data_no_type});
let raw_data: HashMap<String, Value> =
serde_json::from_value(default_entities_data).unwrap();
let err = parse_default_entities_with_warns(Some(raw_data))
.expect_err("Expected error when uid.type is missing");
assert_eq!(err.entry_id, "test456", "Expected entry_id to be 'test456'");
assert!(
matches!(*err.error, ParseEntityErrorKind::InvalidUidTypeField),
"Expected error to be InvalidUidTypeField"
);
}
#[test]
fn test_parse_entity_with_empty_attrs_and_parents() {
let entity_data = json!({
"uid": {
"type": "Test::EmptyTest",
"id": "test789"
},
"attrs": {},
"parents": []
});
let default_entities_data = json!({"test789".to_string(): entity_data});
let raw_data: HashMap<String, Value> =
serde_json::from_value(default_entities_data).unwrap();
let parsed_entities = parse_default_entities_with_warns(Some(raw_data))
.expect("should parse with empty attrs and parents");
let entities = parsed_entities.entities();
let uid = EntityUid::from_str("Test::EmptyTest::\"test789\"").unwrap();
let entity = entities.get(&uid).expect("should have entity");
assert_eq!(
entity.uid().type_name().to_string(),
"Test::EmptyTest",
"Entity type should have namespace prefix"
);
let entity_json = entity.to_json_value().expect("should convert to JSON");
let attrs = entity_json.get("attrs").expect("should have attrs");
assert_eq!(
attrs.as_object().unwrap().len(),
0,
"Entity should have empty attrs"
);
}
#[test]
fn test_entry_id_validation_empty_and_whitespace() {
let entity_data = json!({
"uid": {
"type": "Test::Type",
"id": "test"
},
"attrs": {}
});
let default_entities_data = json!({String::new(): entity_data});
let raw_data: HashMap<String, Value> =
serde_json::from_value(default_entities_data).unwrap();
let err = parse_default_entities_with_warns(Some(raw_data))
.expect_err("Expected error for empty entry ID");
assert_eq!(err.entry_id, "", "Expected entry_id to be empty");
assert!(
matches!(*err.error, ParseEntityErrorKind::EntityIdIsEmpty),
"Expected error to be EntityIdIsEmpty"
);
let default_entities_data_whitespace = json!({" ".to_string(): entity_data});
let raw_data: HashMap<String, Value> =
serde_json::from_value(default_entities_data_whitespace).unwrap();
let err = parse_default_entities_with_warns(Some(raw_data))
.expect_err("Expected error for whitespace-only entry ID");
assert_eq!(
err.entry_id, " ",
"Expected entry_id to be whitespace-only"
);
assert!(
matches!(*err.error, ParseEntityErrorKind::EntityIdIsEmpty),
"Expected error to be EntityIdIsEmpty"
);
}
#[test]
fn test_entry_id_validation_dangerous_patterns() {
let entity_data = json!({
"uid": {
"type": "Test::Type",
"id": "test"
},
"attrs": {}
});
for pattern in DANGEROUS_PATTERNS {
let dangerous_id = format!("prefix{pattern}suffix");
let default_entities_data = json!({dangerous_id.clone(): entity_data.clone()});
let raw_data: HashMap<String, Value> =
serde_json::from_value(default_entities_data).unwrap();
let err = parse_default_entities_with_warns(Some(raw_data)).expect_err(&format!(
"Should return error for dangerous pattern: {pattern}"
));
assert_eq!(
err.entry_id, dangerous_id,
"Expected entry_id to match dangerous pattern"
);
assert!(
matches!(*err.error, ParseEntityErrorKind::EntityIdIsDangerous),
"Expected error to be EntityIdIsDangerous"
);
}
}
#[test]
fn test_valid_entry_ids() {
let valid_ids = [
"normal_id",
"id_with_underscore",
"id-with-dash",
"id123",
"ID_IN_UPPERCASE",
"id.with.dots",
];
for valid_id in valid_ids {
let entity_data = json!({
"uid": {
"type": "Test::Type",
"id": valid_id
},
"attrs": {}
});
let default_entities_data = json!({valid_id.to_string(): entity_data.clone()});
let raw_data: HashMap<String, Value> =
serde_json::from_value(default_entities_data).unwrap();
let parsed_entities = parse_default_entities_with_warns(Some(raw_data))
.unwrap_or_else(|_| panic!("Should parse valid entry ID: {valid_id}"));
assert_eq!(parsed_entities.entities().len(), 1, "Should have 1 entity");
}
}
#[test]
fn test_base64_parsing_valid() {
let entity_json = json!({
"uid": {
"type": "Test::Base64Type",
"id": "base64_test"
},
"attrs": {
"test_attr": "test_value"
}
});
let entity_json_str = entity_json.to_string();
let b64_encoded = base64::prelude::BASE64_STANDARD.encode(entity_json_str);
let default_entities_data = json!({
"base64_entity".to_string(): b64_encoded
});
let raw_data: HashMap<String, Value> =
serde_json::from_value(default_entities_data).unwrap();
let parsed_entities = parse_default_entities_with_warns(Some(raw_data))
.expect("Should parse valid base64 entity");
assert_eq!(parsed_entities.entities().len(), 1, "Should have 1 entity");
let uid = EntityUid::from_str("Test::Base64Type::\"base64_test\"").unwrap();
let entity = parsed_entities
.entities()
.get(&uid)
.expect("should have entity");
assert_eq!(entity.uid().type_name().to_string(), "Test::Base64Type");
}
#[test]
fn test_base64_parsing_invalid_base64() {
let invalid_b64 = "not-valid-base64==";
let default_entities_data = json!({
"invalid_base64_entity".to_string(): invalid_b64
});
let raw_data: HashMap<String, Value> =
serde_json::from_value(default_entities_data).unwrap();
let err = parse_default_entities_with_warns(Some(raw_data))
.expect_err("Expected error for invalid base64");
assert_eq!(
err.entry_id, "invalid_base64_entity",
"Expected entry_id to be 'invalid_base64_entity'"
);
assert!(
matches!(*err.error, ParseEntityErrorKind::Base64Decode(_)),
"Expected error to be Base64Decode"
);
}
#[test]
fn test_base64_parsing_invalid_json_after_decode() {
let invalid_json = "not valid json";
let b64_encoded = base64::prelude::BASE64_STANDARD.encode(invalid_json);
let default_entities_data = json!({
"invalid_json_entity".to_string(): b64_encoded
});
let raw_data: HashMap<String, Value> =
serde_json::from_value(default_entities_data).unwrap();
let err = parse_default_entities_with_warns(Some(raw_data))
.expect_err("Expected error for invalid JSON after base64 decode");
assert_eq!(
err.entry_id, "invalid_json_entity",
"Expected entry_id to be 'invalid_json_entity'"
);
assert!(
matches!(*err.error, ParseEntityErrorKind::Base64DecodedIsNotJson(_)),
"Expected error to be Base64DecodedIsNotJson"
);
}
#[test]
fn test_base64_parsing_non_utf8_content() {
let non_utf8_bytes = vec![0xFF, 0xFE, 0x00]; let b64_encoded = base64::prelude::BASE64_STANDARD.encode(non_utf8_bytes);
let default_entities_data = json!({
"non_utf8_entity".to_string(): b64_encoded
});
let raw_data: HashMap<String, Value> =
serde_json::from_value(default_entities_data).unwrap();
let err = parse_default_entities_with_warns(Some(raw_data))
.expect_err("Expected error for non-UTF8 content after base64 decode");
assert_eq!(
err.entry_id, "non_utf8_entity",
"Expected entry_id to be 'non_utf8_entity'"
);
assert!(
matches!(*err.error, ParseEntityErrorKind::UnicodeDecode(_)),
"Expected error to be UnicodeDecode"
);
}
#[test]
fn test_namespace_handling() {
let entity_with_namespace = json!({
"uid": {
"type": "Custom::Namespace::EntityType",
"id": "test1"
},
"attrs": {}
});
let entity_without_namespace = json!({
"uid": {
"type": "SimpleType",
"id": "test2"
},
"attrs": {}
});
let default_entities_data = json!({
"test1".to_string(): entity_with_namespace,
"test2".to_string(): entity_without_namespace
});
let raw_data: HashMap<String, Value> =
serde_json::from_value(default_entities_data).unwrap();
let parsed_entities = parse_default_entities_with_warns(Some(raw_data))
.expect("Should parse entities with and without namespaces");
assert_eq!(
parsed_entities.entities().len(),
2,
"Should have 2 entities"
);
let uid1 = EntityUid::from_str("Custom::Namespace::EntityType::\"test1\"").unwrap();
let entity1 = parsed_entities
.entities()
.get(&uid1)
.expect("should have entity1");
assert_eq!(
entity1.uid().type_name().to_string(),
"Custom::Namespace::EntityType"
);
let uid2 = EntityUid::from_str("SimpleType::\"test2\"").unwrap();
let entity2 = parsed_entities
.entities()
.get(&uid2)
.expect("should have entity2");
assert_eq!(entity2.uid().type_name().to_string(), "SimpleType");
}
#[test]
fn test_legacy_format_parsing() {
let legacy_entity = json!({
"entity_type": "Legacy::Type",
"entity_id": "legacy_test",
"custom_attr": "custom_value"
});
let default_entities_data = json!({
"legacy_entity".to_string(): legacy_entity
});
let raw_data: HashMap<String, Value> =
serde_json::from_value(default_entities_data).unwrap();
let parsed_entities = parse_default_entities_with_warns(Some(raw_data))
.expect("Should parse legacy format entity");
assert_eq!(parsed_entities.entities().len(), 1, "Should have 1 entity");
let uid = EntityUid::from_str("Legacy::Type::\"legacy_test\"").unwrap();
let entity = parsed_entities
.entities()
.get(&uid)
.expect("should have entity");
assert_eq!(entity.uid().type_name().to_string(), "Legacy::Type");
let entity_json = entity.to_json_value().expect("should convert to JSON");
let attrs = entity_json.get("attrs").expect("should have attrs");
let custom_attr = attrs.get("custom_attr").expect("should have custom_attr");
assert_eq!(custom_attr.as_str().unwrap(), "custom_value");
}
#[test]
fn test_parent_entity_parsing() {
let entity_with_parents = json!({
"uid": {
"type": "Test::ChildType",
"id": "child_entity"
},
"attrs": {},
"parents": [
{
"type": "Test::ParentType1",
"id": "parent1"
},
{
"type": "Test::ParentType2",
"id": "parent2"
}
]
});
let default_entities_data = json!({
"child_entity".to_string(): entity_with_parents
});
let raw_data: HashMap<String, Value> =
serde_json::from_value(default_entities_data).unwrap();
let parsed_entities = parse_default_entities_with_warns(Some(raw_data))
.expect("Should parse entity with parents");
assert_eq!(parsed_entities.entities().len(), 1, "Should have 1 entity");
let uid = EntityUid::from_str("Test::ChildType::\"child_entity\"").unwrap();
let entity = parsed_entities
.entities()
.get(&uid)
.expect("should have entity");
let _ = entity;
}
#[test]
fn test_parent_entity_parsing_with_warnings() {
let entity_with_invalid_parents = json!({
"uid": {
"type": "Test::Type",
"id": "test_entity"
},
"attrs": {},
"parents": [
{
"type": "ValidParent",
"id": "valid"
},
{
"type": "InvalidParent",
"id": "invalid@uid" },
"not_an_object", {
"missing_type": "parent", "id": "parent3"
}
]
});
let default_entities_data = json!({
"test_entity".to_string(): entity_with_invalid_parents
});
let raw_data: HashMap<String, Value> =
serde_json::from_value(default_entities_data).unwrap();
let parsed_entities = parse_default_entities_with_warns(Some(raw_data))
.expect("Should parse entity with invalid parents (but generate warnings)");
assert_eq!(parsed_entities.entities().len(), 1, "Should have 1 entity");
assert!(!parsed_entities.warns().is_empty(), "Should have warnings");
let uid = EntityUid::from_str("Test::Type::\"test_entity\"").unwrap();
let _entity = parsed_entities
.entities()
.get(&uid)
.expect("should have entity");
}
#[test]
fn test_attribute_parsing_various_types() {
let entity_with_attrs = json!({
"uid": {
"type": "Test::AttrType",
"id": "attr_test"
},
"attrs": {
"string_attr": "string_value",
"number_attr": 42,
"bool_attr": true,
"array_attr": ["item1", "item2"],
"object_attr": {"nested": "value"}
}
});
let default_entities_data = json!({
"attr_test".to_string(): entity_with_attrs
});
let raw_data: HashMap<String, Value> =
serde_json::from_value(default_entities_data).unwrap();
let parsed_entities = parse_default_entities_with_warns(Some(raw_data))
.expect("Should parse entity with various attribute types");
assert_eq!(parsed_entities.entities().len(), 1, "Should have 1 entity");
let uid = EntityUid::from_str("Test::AttrType::\"attr_test\"").unwrap();
let entity = parsed_entities
.entities()
.get(&uid)
.expect("should have entity");
let entity_json = entity.to_json_value().expect("should convert to JSON");
let attrs = entity_json.get("attrs").expect("should have attrs");
let attrs_obj = attrs.as_object().expect("attrs should be object");
assert_eq!(attrs_obj.len(), 5, "Should have 5 attributes");
assert_eq!(
attrs_obj.get("string_attr").unwrap().as_str().unwrap(),
"string_value"
);
assert_eq!(attrs_obj.get("number_attr").unwrap().as_i64().unwrap(), 42);
assert_eq!(attrs_obj.get("bool_attr").unwrap().as_bool().unwrap(), true);
}
#[test]
fn test_entity_without_uid_id_falls_back_to_entry_id() {
let entity_without_uid_id = json!({
"uid": {
"type": "Test::FallbackType"
},
"attrs": {}
});
let entry_id = "fallback_entry_id";
let default_entities_data = json!({entry_id.to_string(): entity_without_uid_id});
let raw_data: HashMap<String, Value> =
serde_json::from_value(default_entities_data).unwrap();
let parsed_entities = parse_default_entities_with_warns(Some(raw_data))
.expect("Should parse entity with fallback ID");
assert_eq!(parsed_entities.entities().len(), 1, "Should have 1 entity");
let uid = EntityUid::from_str("Test::FallbackType::\"fallback_entry_id\"").unwrap();
let entity = parsed_entities
.entities()
.get(&uid)
.expect("should have entity");
assert_eq!(entity.uid().id().as_ref() as &str, "fallback_entry_id");
}
#[test]
fn test_empty_default_entities() {
let parsed_entities =
parse_default_entities_with_warns(None).expect("Should parse empty default entities");
assert_eq!(
parsed_entities.entities().len(),
0,
"Should have 0 entities"
);
assert!(
parsed_entities.warns().is_empty(),
"Should have no warnings"
);
}
#[test]
fn test_invalid_value_type_error() {
let default_entities_data = json!({
"invalid_entity".to_string(): 12345 });
let raw_data: HashMap<String, Value> =
serde_json::from_value(default_entities_data).unwrap();
let err = parse_default_entities_with_warns(Some(raw_data))
.expect_err("Expected error for invalid value type");
assert_eq!(
err.entry_id, "invalid_entity",
"Expected entry_id to be 'invalid_entity'"
);
assert!(
matches!(*err.error, ParseEntityErrorKind::IsNotJsonObject),
"Expected error to be IsNotJsonObject"
);
}
#[test]
fn test_mixed_format_entities() {
let base64_entity_json = json!({
"uid": {
"type": "Test::Base64Type",
"id": "base64_entity"
},
"attrs": {
"source": "base64"
}
});
let base64_encoded =
base64::prelude::BASE64_STANDARD.encode(base64_entity_json.to_string());
let json_entity = json!({
"uid": {
"type": "Test::JsonType",
"id": "json_entity"
},
"attrs": {
"source": "json"
}
});
let default_entities_data = json!({
"base64_entity".to_string(): base64_encoded,
"json_entity".to_string(): json_entity
});
let raw_data: HashMap<String, Value> =
serde_json::from_value(default_entities_data).unwrap();
let parsed_entities = parse_default_entities_with_warns(Some(raw_data))
.expect("Should parse mixed format entities");
assert_eq!(
parsed_entities.entities().len(),
2,
"Should have 2 entities"
);
let base64_uid = EntityUid::from_str("Test::Base64Type::\"base64_entity\"").unwrap();
let json_uid = EntityUid::from_str("Test::JsonType::\"json_entity\"").unwrap();
assert!(
parsed_entities.entities().get(&base64_uid).is_some(),
"Should have base64 entity"
);
assert!(
parsed_entities.entities().get(&json_uid).is_some(),
"Should have json entity"
);
}
#[test]
fn test_warning_enum_invalid_parent_uid() {
let entity_with_invalid_parent = json!({
"uid": {
"type": "Test::Type",
"id": "test_entity"
},
"attrs": {},
"parents": [
{
"type": "InvalidParent",
"id": "invalid@uid" }
]
});
let default_entities_data = json!({
"test_entity".to_string(): entity_with_invalid_parent
});
let raw_data: HashMap<String, Value> =
serde_json::from_value(default_entities_data).unwrap();
let parsed_entities = parse_default_entities_with_warns(Some(raw_data))
.expect("Should parse entity with invalid parent");
assert!(
parsed_entities.warns().is_empty(),
"Should have no warnings for valid UID"
);
}
#[test]
fn test_warning_enum_non_object_parent_entry() {
let entity_with_invalid_parents = json!({
"uid": {
"type": "Test::Type",
"id": "test_entity"
},
"attrs": {},
"parents": [
"not_an_object", {
"type": "ValidParent",
"id": "valid"
}
]
});
let default_entities_data = json!({
"test_entity".to_string(): entity_with_invalid_parents
});
let raw_data: HashMap<String, Value> =
serde_json::from_value(default_entities_data).unwrap();
let parsed_entities = parse_default_entities_with_warns(Some(raw_data))
.expect("Should parse entity with invalid parents");
assert!(!parsed_entities.warns().is_empty(), "Should have warnings");
let warnings = parsed_entities.warns();
assert_eq!(warnings.len(), 1, "Should have exactly 1 warning");
match &warnings[0] {
DefaultEntityWarning::NonObjectParentEntry { entry_id, value } => {
assert_eq!(entry_id, "test_entity");
assert!(value.contains("not_an_object"));
},
DefaultEntityWarning::InvalidParentUid { .. } => panic!(
"Expected NonObjectParentEntry warning, got {:?}",
warnings[0]
),
}
}
#[test]
fn test_warning_enum_non_object_parent() {
let entity_with_invalid_parents = json!({
"uid": {
"type": "Test::Type",
"id": "test_entity"
},
"attrs": {},
"parents": [
"not_an_object", {
"missing_type": "parent", "id": "parent3"
}
]
});
let default_entities_data = json!({
"test_entity".to_string(): entity_with_invalid_parents
});
let raw_data: HashMap<String, Value> =
serde_json::from_value(default_entities_data).unwrap();
let parsed_entities = parse_default_entities_with_warns(Some(raw_data))
.expect("Should parse entity with invalid parents");
assert!(!parsed_entities.warns().is_empty(), "Should have warnings");
let warnings = parsed_entities.warns();
assert_eq!(warnings.len(), 2, "Should have exactly 2 warnings");
for warning in warnings {
match warning {
DefaultEntityWarning::NonObjectParentEntry { entry_id, value } => {
assert_eq!(entry_id, "test_entity");
assert!(!value.is_empty());
},
DefaultEntityWarning::InvalidParentUid { .. } => {
panic!("Expected NonObjectParentEntry warning, got {warning:?}")
},
}
}
}
#[test]
fn test_warning_enum_multiple_warnings() {
let entity_with_multiple_issues = json!({
"uid": {
"type": "Test::Type",
"id": "test_entity"
},
"attrs": {},
"parents": [
"not_an_object", {
"missing_type": "parent", "id": "parent3"
}
]
});
let default_entities_data = json!({
"test_entity".to_string(): entity_with_multiple_issues
});
let raw_data: HashMap<String, Value> =
serde_json::from_value(default_entities_data).unwrap();
let parsed_entities = parse_default_entities_with_warns(Some(raw_data))
.expect("Should parse entity with multiple issues");
let warnings = parsed_entities.warns();
assert_eq!(warnings.len(), 2, "Should have exactly 2 warnings");
for warning in warnings {
match warning {
DefaultEntityWarning::NonObjectParentEntry { entry_id, value } => {
assert_eq!(entry_id, "test_entity");
assert!(!value.is_empty());
},
DefaultEntityWarning::InvalidParentUid { .. } => {
panic!("Expected NonObjectParentEntry warning, got {warning:?}")
},
}
}
}
#[test]
fn test_warning_enum_display_format() {
let warning = DefaultEntityWarning::InvalidParentUid {
entry_id: "test_entity".to_string(),
parent_uid_str: "Test::Parent::\"invalid@uid\"".to_string(),
error: "invalid character".to_string(),
};
let display_string = warning.to_string();
assert!(display_string.contains("test_entity"));
assert!(display_string.contains("Test::Parent"));
assert!(display_string.contains("invalid character"));
let warning2 = DefaultEntityWarning::NonObjectParentEntry {
entry_id: "test_entity".to_string(),
value: "\"not_an_object\"".to_string(),
};
let display_string2 = warning2.to_string();
assert!(display_string2.contains("test_entity"));
assert!(display_string2.contains("not_an_object"));
}
}