Public Access
initial-commit
This commit is contained in:
@@ -0,0 +1,727 @@
|
||||
use crate::charybdis::entities::Entity;
|
||||
use crate::database::EntityRepository;
|
||||
use anyhow::{Result, anyhow};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tracing::warn;
|
||||
|
||||
/// Field mapper handles mapping between entity fields and external tool fields
|
||||
pub struct FieldMapper {
|
||||
mappings: HashMap<String, FieldMapping>,
|
||||
repository: Option<Arc<EntityRepository>>,
|
||||
}
|
||||
|
||||
/// Field mapping types
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum FieldMapping {
|
||||
/// Complex mapping with entity resolution (must come first to match objects with "from" key)
|
||||
Complex(ComplexMapping),
|
||||
|
||||
/// Static value wrapper (object with "value" key)
|
||||
/// Example: {"value": "Web Application"}, {"value": true}, {"value": 123}
|
||||
StaticWrapper(StaticValue),
|
||||
|
||||
/// Direct field access using dot notation (plain string)
|
||||
/// Example: "metadata.name" accesses entity.metadata.name
|
||||
Direct(String),
|
||||
}
|
||||
|
||||
/// Wrapper for static values to distinguish from field paths
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StaticValue {
|
||||
pub value: Value,
|
||||
}
|
||||
|
||||
/// Complex field mapping configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ComplexMapping {
|
||||
/// Source field path
|
||||
pub from: String,
|
||||
|
||||
/// Optional: Resolve entity reference
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resolve_entity: Option<String>,
|
||||
|
||||
/// Optional: Find linked entity of this kind
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub lookup_entity: Option<String>,
|
||||
|
||||
/// Optional: Extract field from resolved entity
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extract: Option<String>,
|
||||
|
||||
/// Optional: Resolve array of entity IDs
|
||||
#[serde(default)]
|
||||
pub resolve_array: bool,
|
||||
|
||||
/// Optional: Transform function
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub transform: Option<String>,
|
||||
}
|
||||
|
||||
impl FieldMapper {
|
||||
/// Create a new field mapper from configuration
|
||||
/// Config format: { "target_field": "source.field" or complex mapping }
|
||||
pub fn new(config: HashMap<String, Value>) -> Result<Self> {
|
||||
let mut mappings = HashMap::new();
|
||||
|
||||
for (field_name, mapping_value) in config {
|
||||
let mapping = Self::parse_mapping(mapping_value)?;
|
||||
mappings.insert(field_name, mapping);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
mappings,
|
||||
repository: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Set the entity repository (required for entity resolution)
|
||||
pub fn with_repository(mut self, repository: Arc<EntityRepository>) -> Self {
|
||||
self.repository = Some(repository);
|
||||
self
|
||||
}
|
||||
|
||||
/// Parse a mapping value from config
|
||||
fn parse_mapping(value: Value) -> Result<FieldMapping> {
|
||||
// Use serde untagged deserialization
|
||||
// Order matters: Complex (with "from"), StaticWrapper (with "value"), Direct (string)
|
||||
serde_json::from_value(value).map_err(|e| anyhow!("Failed to parse field mapping: {}", e))
|
||||
}
|
||||
|
||||
/// Map a single field from source entity (async for entity resolution)
|
||||
pub async fn map(&self, field_name: &str, entity: &Entity) -> Result<Value> {
|
||||
let mapping = self
|
||||
.mappings
|
||||
.get(field_name)
|
||||
.ok_or_else(|| anyhow!("No mapping configured for field: {}", field_name))?;
|
||||
|
||||
match mapping {
|
||||
FieldMapping::Direct(path) => self.get_field_by_path(entity, path),
|
||||
|
||||
FieldMapping::StaticWrapper(static_val) => Ok(static_val.value.clone()),
|
||||
|
||||
FieldMapping::Complex(complex) => self.map_complex(complex, entity).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle complex field mapping with entity resolution
|
||||
async fn map_complex(&self, mapping: &ComplexMapping, entity: &Entity) -> Result<Value> {
|
||||
// Get source field value
|
||||
let source_value = self.get_field_by_path(entity, &mapping.from)?;
|
||||
|
||||
// If no entity resolution needed, just return (possibly transformed) value
|
||||
if mapping.resolve_entity.is_none() && !mapping.resolve_array {
|
||||
return Ok(source_value);
|
||||
}
|
||||
|
||||
// Entity resolution required - need repository
|
||||
let repository = self
|
||||
.repository
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("Entity resolution requires repository"))?;
|
||||
|
||||
// Handle array resolution
|
||||
if mapping.resolve_array {
|
||||
return self.resolve_array(source_value, mapping, repository).await;
|
||||
}
|
||||
|
||||
// Handle single entity resolution
|
||||
if let Some(resolve_kind) = &mapping.resolve_entity {
|
||||
return self
|
||||
.resolve_entity(source_value, resolve_kind, mapping, repository)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(source_value)
|
||||
}
|
||||
|
||||
/// Resolve a single entity reference
|
||||
async fn resolve_entity(
|
||||
&self,
|
||||
entity_id: Value,
|
||||
expected_kind: &str,
|
||||
mapping: &ComplexMapping,
|
||||
repository: &EntityRepository,
|
||||
) -> Result<Value> {
|
||||
let id_str = entity_id
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow!("Entity ID must be a string"))?;
|
||||
|
||||
// Resolve the entity
|
||||
let resolved = repository
|
||||
.get_by_id(id_str)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("Entity not found: {}", id_str))?;
|
||||
|
||||
// Validate kind if specified
|
||||
if resolved.kind != expected_kind {
|
||||
return Err(anyhow!(
|
||||
"Expected entity kind {}, got {}",
|
||||
expected_kind,
|
||||
resolved.kind
|
||||
));
|
||||
}
|
||||
|
||||
// If we need to lookup a linked entity
|
||||
if let Some(lookup_kind) = &mapping.lookup_entity {
|
||||
let linked = self
|
||||
.find_linked_entity(&resolved.id, lookup_kind, repository)
|
||||
.await?;
|
||||
|
||||
// Extract field from linked entity
|
||||
if let Some(extract_path) = &mapping.extract {
|
||||
return self.get_field_by_path(&linked, extract_path);
|
||||
}
|
||||
|
||||
return entity_to_json(&linked);
|
||||
}
|
||||
|
||||
// Extract field from resolved entity
|
||||
if let Some(extract_path) = &mapping.extract {
|
||||
return self.get_field_by_path(&resolved, extract_path);
|
||||
}
|
||||
|
||||
// Return entire resolved entity
|
||||
entity_to_json(&resolved)
|
||||
}
|
||||
|
||||
/// Resolve an array of entity references
|
||||
async fn resolve_array(
|
||||
&self,
|
||||
entity_ids: Value,
|
||||
mapping: &ComplexMapping,
|
||||
repository: &EntityRepository,
|
||||
) -> Result<Value> {
|
||||
let ids_array = entity_ids
|
||||
.as_array()
|
||||
.ok_or_else(|| anyhow!("Expected array of entity IDs"))?;
|
||||
|
||||
let mut results = Vec::new();
|
||||
|
||||
for id_value in ids_array {
|
||||
let resolve_kind = mapping
|
||||
.resolve_entity
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("resolve_entity required for array resolution"))?;
|
||||
|
||||
match self
|
||||
.resolve_entity(id_value.clone(), resolve_kind, mapping, repository)
|
||||
.await
|
||||
{
|
||||
Ok(value) => results.push(value),
|
||||
Err(e) => {
|
||||
warn!("Failed to resolve entity in array: {}", e);
|
||||
// Continue with other entities
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Value::Array(results))
|
||||
}
|
||||
|
||||
/// Find an entity linked to a source entity
|
||||
async fn find_linked_entity(
|
||||
&self,
|
||||
source_id: &str,
|
||||
target_kind: &str,
|
||||
repository: &EntityRepository,
|
||||
) -> Result<Entity> {
|
||||
let entities = repository.list_by_kind(target_kind).await?;
|
||||
|
||||
// Find entity with linked_entity_id matching source_id
|
||||
for entity in entities {
|
||||
let linked_id = self
|
||||
.get_field_by_path(&entity, "metadata.linked_entity_id")
|
||||
.ok()
|
||||
.and_then(|v| v.as_str().map(String::from));
|
||||
|
||||
if linked_id.as_deref() == Some(source_id) {
|
||||
return Ok(entity);
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow!(
|
||||
"No {} entity found linked to {}",
|
||||
target_kind,
|
||||
source_id
|
||||
))
|
||||
}
|
||||
|
||||
/// Get field value from entity using dot notation path
|
||||
/// Example: "metadata.name" → entity.metadata.name
|
||||
fn get_field_by_path(&self, entity: &Entity, path: &str) -> Result<Value> {
|
||||
// Convert entity to JSON for easy path traversal
|
||||
let entity_json = entity_to_json(entity)?;
|
||||
|
||||
// Split path and traverse JSON
|
||||
let parts: Vec<&str> = path.split('.').collect();
|
||||
let mut current = &entity_json;
|
||||
|
||||
for part in parts {
|
||||
current = current
|
||||
.get(part)
|
||||
.ok_or_else(|| anyhow!("Field not found in entity: {}", path))?;
|
||||
}
|
||||
|
||||
Ok(current.clone())
|
||||
}
|
||||
|
||||
/// Map all configured fields from entity to target format
|
||||
/// Returns HashMap of field_name → mapped_value
|
||||
pub async fn map_all(&self, entity: &Entity) -> Result<HashMap<String, Value>> {
|
||||
let mut result = HashMap::new();
|
||||
|
||||
for (field_name, _) in &self.mappings {
|
||||
match self.map(field_name, entity).await {
|
||||
Ok(value) => {
|
||||
result.insert(field_name.clone(), value);
|
||||
}
|
||||
Err(e) => {
|
||||
// Log warning but don't fail entire mapping
|
||||
warn!("Failed to map field {}: {}", field_name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Check if a field is mapped
|
||||
pub fn has_mapping(&self, field_name: &str) -> bool {
|
||||
self.mappings.contains_key(field_name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to convert Entity to JSON Value
|
||||
/// Works around prost_types::Timestamp not implementing serde traits
|
||||
fn entity_to_json(entity: &Entity) -> Result<Value> {
|
||||
use crate::charybdis::entities::entity;
|
||||
use serde_json::json;
|
||||
|
||||
// Manual conversion to avoid prost_types::Timestamp serde issues
|
||||
let mut entity_obj = serde_json::Map::new();
|
||||
|
||||
entity_obj.insert("id".to_string(), json!(&entity.id));
|
||||
entity_obj.insert("kind".to_string(), json!(&entity.kind));
|
||||
|
||||
// Add metadata if present - extract fields based on metadata type
|
||||
if let Some(ref metadata) = entity.metadata {
|
||||
let mut metadata_obj = serde_json::Map::new();
|
||||
|
||||
match metadata {
|
||||
entity::Metadata::ServiceMetadata(m) => {
|
||||
metadata_obj.insert("name".to_string(), json!(&m.name));
|
||||
if !m.namespace.is_empty() {
|
||||
metadata_obj.insert("namespace".to_string(), json!(&m.namespace));
|
||||
}
|
||||
if !m.description.is_empty() {
|
||||
metadata_obj.insert("description".to_string(), json!(&m.description));
|
||||
}
|
||||
// ServiceMetadata labels is repeated string (Vec<String>)
|
||||
if !m.labels.is_empty() {
|
||||
metadata_obj.insert("labels".to_string(), json!(&m.labels));
|
||||
}
|
||||
if !m.tags.is_empty() {
|
||||
metadata_obj.insert("tags".to_string(), json!(&m.tags));
|
||||
}
|
||||
}
|
||||
entity::Metadata::SystemMetadata(m) => {
|
||||
metadata_obj.insert("name".to_string(), json!(&m.name));
|
||||
if !m.namespace.is_empty() {
|
||||
metadata_obj.insert("namespace".to_string(), json!(&m.namespace));
|
||||
}
|
||||
if !m.description.is_empty() {
|
||||
metadata_obj.insert("description".to_string(), json!(&m.description));
|
||||
}
|
||||
if !m.labels.is_empty() {
|
||||
metadata_obj.insert("labels".to_string(), json!(&m.labels));
|
||||
}
|
||||
if !m.tags.is_empty() {
|
||||
metadata_obj.insert("tags".to_string(), json!(&m.tags));
|
||||
}
|
||||
}
|
||||
entity::Metadata::ComponentMetadata(m) => {
|
||||
metadata_obj.insert("name".to_string(), json!(&m.name));
|
||||
if !m.namespace.is_empty() {
|
||||
metadata_obj.insert("namespace".to_string(), json!(&m.namespace));
|
||||
}
|
||||
if !m.description.is_empty() {
|
||||
metadata_obj.insert("description".to_string(), json!(&m.description));
|
||||
}
|
||||
if !m.labels.is_empty() {
|
||||
metadata_obj.insert("labels".to_string(), json!(&m.labels));
|
||||
}
|
||||
if !m.tags.is_empty() {
|
||||
metadata_obj.insert("tags".to_string(), json!(&m.tags));
|
||||
}
|
||||
}
|
||||
entity::Metadata::ApiMetadata(m) => {
|
||||
metadata_obj.insert("name".to_string(), json!(&m.name));
|
||||
if !m.namespace.is_empty() {
|
||||
metadata_obj.insert("namespace".to_string(), json!(&m.namespace));
|
||||
}
|
||||
if !m.description.is_empty() {
|
||||
metadata_obj.insert("description".to_string(), json!(&m.description));
|
||||
}
|
||||
if !m.labels.is_empty() {
|
||||
metadata_obj.insert("labels".to_string(), json!(&m.labels));
|
||||
}
|
||||
if !m.tags.is_empty() {
|
||||
metadata_obj.insert("tags".to_string(), json!(&m.tags));
|
||||
}
|
||||
}
|
||||
entity::Metadata::UserMetadata(m) => {
|
||||
metadata_obj.insert("name".to_string(), json!(&m.name));
|
||||
if !m.namespace.is_empty() {
|
||||
metadata_obj.insert("namespace".to_string(), json!(&m.namespace));
|
||||
}
|
||||
if !m.description.is_empty() {
|
||||
metadata_obj.insert("description".to_string(), json!(&m.description));
|
||||
}
|
||||
if !m.labels.is_empty() {
|
||||
metadata_obj.insert("labels".to_string(), json!(&m.labels));
|
||||
}
|
||||
if !m.tags.is_empty() {
|
||||
metadata_obj.insert("tags".to_string(), json!(&m.tags));
|
||||
}
|
||||
}
|
||||
entity::Metadata::GroupMetadata(m) => {
|
||||
metadata_obj.insert("name".to_string(), json!(&m.name));
|
||||
if !m.namespace.is_empty() {
|
||||
metadata_obj.insert("namespace".to_string(), json!(&m.namespace));
|
||||
}
|
||||
if !m.description.is_empty() {
|
||||
metadata_obj.insert("description".to_string(), json!(&m.description));
|
||||
}
|
||||
if !m.labels.is_empty() {
|
||||
metadata_obj.insert("labels".to_string(), json!(&m.labels));
|
||||
}
|
||||
if !m.tags.is_empty() {
|
||||
metadata_obj.insert("tags".to_string(), json!(&m.tags));
|
||||
}
|
||||
}
|
||||
entity::Metadata::DomainMetadata(m) => {
|
||||
metadata_obj.insert("name".to_string(), json!(&m.name));
|
||||
if !m.namespace.is_empty() {
|
||||
metadata_obj.insert("namespace".to_string(), json!(&m.namespace));
|
||||
}
|
||||
if !m.description.is_empty() {
|
||||
metadata_obj.insert("description".to_string(), json!(&m.description));
|
||||
}
|
||||
if !m.labels.is_empty() {
|
||||
metadata_obj.insert("labels".to_string(), json!(&m.labels));
|
||||
}
|
||||
if !m.tags.is_empty() {
|
||||
metadata_obj.insert("tags".to_string(), json!(&m.tags));
|
||||
}
|
||||
}
|
||||
entity::Metadata::ResourceMetadata(m) => {
|
||||
metadata_obj.insert("name".to_string(), json!(&m.name));
|
||||
if !m.namespace.is_empty() {
|
||||
metadata_obj.insert("namespace".to_string(), json!(&m.namespace));
|
||||
}
|
||||
if !m.description.is_empty() {
|
||||
metadata_obj.insert("description".to_string(), json!(&m.description));
|
||||
}
|
||||
if !m.labels.is_empty() {
|
||||
metadata_obj.insert("labels".to_string(), json!(&m.labels));
|
||||
}
|
||||
if !m.tags.is_empty() {
|
||||
metadata_obj.insert("tags".to_string(), json!(&m.tags));
|
||||
}
|
||||
}
|
||||
entity::Metadata::DefectdojoMetadata(_) => {
|
||||
// Plugin type — skip for now
|
||||
}
|
||||
entity::Metadata::KeycloakMetadata(_) => {
|
||||
// Plugin metadata type — skip
|
||||
}
|
||||
entity::Metadata::DependencytrackMetadata(_) => {
|
||||
// Plugin metadata type — skip
|
||||
}
|
||||
entity::Metadata::FindingMetadata(m) => {
|
||||
metadata_obj.insert("name".to_string(), json!(&m.title));
|
||||
if !m.description.is_empty() {
|
||||
metadata_obj.insert("description".to_string(), json!(&m.description));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entity_obj.insert("metadata".to_string(), Value::Object(metadata_obj));
|
||||
}
|
||||
|
||||
// Add spec if present - extract fields based on spec type
|
||||
if let Some(ref spec) = entity.spec {
|
||||
let mut spec_obj = serde_json::Map::new();
|
||||
|
||||
match spec {
|
||||
entity::Spec::ServiceSpec(s) => {
|
||||
if !s.r#type.is_empty() {
|
||||
spec_obj.insert("type".to_string(), json!(&s.r#type));
|
||||
}
|
||||
if !s.lifecycle.is_empty() {
|
||||
spec_obj.insert("lifecycle".to_string(), json!(&s.lifecycle));
|
||||
}
|
||||
if !s.owner.is_empty() {
|
||||
spec_obj.insert("owner".to_string(), json!(&s.owner));
|
||||
}
|
||||
if !s.system.is_empty() {
|
||||
spec_obj.insert("system".to_string(), json!(&s.system));
|
||||
}
|
||||
if !s.subcomponent_of.is_empty() {
|
||||
spec_obj.insert("subcomponent_of".to_string(), json!(&s.subcomponent_of));
|
||||
}
|
||||
if !s.depends_on.is_empty() {
|
||||
spec_obj.insert("depends_on".to_string(), json!(&s.depends_on));
|
||||
}
|
||||
if !s.consumes_apis.is_empty() {
|
||||
spec_obj.insert("consumes_apis".to_string(), json!(&s.consumes_apis));
|
||||
}
|
||||
if !s.provides_apis.is_empty() {
|
||||
spec_obj.insert("provides_apis".to_string(), json!(&s.provides_apis));
|
||||
}
|
||||
}
|
||||
entity::Spec::SystemSpec(s) => {
|
||||
if !s.owner.is_empty() {
|
||||
spec_obj.insert("owner".to_string(), json!(&s.owner));
|
||||
}
|
||||
if !s.domain.is_empty() {
|
||||
spec_obj.insert("domain".to_string(), json!(&s.domain));
|
||||
}
|
||||
}
|
||||
entity::Spec::ComponentSpec(s) => {
|
||||
if !s.r#type.is_empty() {
|
||||
spec_obj.insert("type".to_string(), json!(&s.r#type));
|
||||
}
|
||||
if !s.lifecycle.is_empty() {
|
||||
spec_obj.insert("lifecycle".to_string(), json!(&s.lifecycle));
|
||||
}
|
||||
if !s.owner.is_empty() {
|
||||
spec_obj.insert("owner".to_string(), json!(&s.owner));
|
||||
}
|
||||
if !s.system.is_empty() {
|
||||
spec_obj.insert("system".to_string(), json!(&s.system));
|
||||
}
|
||||
if !s.subcomponent_of.is_empty() {
|
||||
spec_obj.insert("subcomponent_of".to_string(), json!(&s.subcomponent_of));
|
||||
}
|
||||
if !s.depends_on.is_empty() {
|
||||
spec_obj.insert("depends_on".to_string(), json!(&s.depends_on));
|
||||
}
|
||||
if !s.provides_apis.is_empty() {
|
||||
spec_obj.insert("provides_apis".to_string(), json!(&s.provides_apis));
|
||||
}
|
||||
if !s.consumes_apis.is_empty() {
|
||||
spec_obj.insert("consumes_apis".to_string(), json!(&s.consumes_apis));
|
||||
}
|
||||
}
|
||||
entity::Spec::ApiSpec(s) => {
|
||||
if !s.r#type.is_empty() {
|
||||
spec_obj.insert("type".to_string(), json!(&s.r#type));
|
||||
}
|
||||
if !s.lifecycle.is_empty() {
|
||||
spec_obj.insert("lifecycle".to_string(), json!(&s.lifecycle));
|
||||
}
|
||||
if !s.owner.is_empty() {
|
||||
spec_obj.insert("owner".to_string(), json!(&s.owner));
|
||||
}
|
||||
if !s.system.is_empty() {
|
||||
spec_obj.insert("system".to_string(), json!(&s.system));
|
||||
}
|
||||
if !s.definition.is_empty() {
|
||||
spec_obj.insert("definition".to_string(), json!(&s.definition));
|
||||
}
|
||||
}
|
||||
entity::Spec::UserSpec(s) => {
|
||||
if let Some(profile) = &s.profile {
|
||||
let mut profile_obj = serde_json::Map::new();
|
||||
if !profile.display_name.is_empty() {
|
||||
profile_obj.insert("display_name".to_string(), json!(&profile.display_name));
|
||||
}
|
||||
if !profile.email.is_empty() {
|
||||
profile_obj.insert("email".to_string(), json!(&profile.email));
|
||||
}
|
||||
if !profile.picture.is_empty() {
|
||||
profile_obj.insert("picture".to_string(), json!(&profile.picture));
|
||||
}
|
||||
if !profile_obj.is_empty() {
|
||||
spec_obj.insert("profile".to_string(), Value::Object(profile_obj));
|
||||
}
|
||||
}
|
||||
if !s.member_of.is_empty() {
|
||||
spec_obj.insert("member_of".to_string(), json!(&s.member_of));
|
||||
}
|
||||
}
|
||||
entity::Spec::GroupSpec(s) => {
|
||||
if !s.r#type.is_empty() {
|
||||
spec_obj.insert("type".to_string(), json!(&s.r#type));
|
||||
}
|
||||
if let Some(profile) = &s.profile {
|
||||
let mut profile_obj = serde_json::Map::new();
|
||||
if !profile.display_name.is_empty() {
|
||||
profile_obj.insert("display_name".to_string(), json!(&profile.display_name));
|
||||
}
|
||||
if !profile.email.is_empty() {
|
||||
profile_obj.insert("email".to_string(), json!(&profile.email));
|
||||
}
|
||||
if !profile.picture.is_empty() {
|
||||
profile_obj.insert("picture".to_string(), json!(&profile.picture));
|
||||
}
|
||||
if !profile_obj.is_empty() {
|
||||
spec_obj.insert("profile".to_string(), Value::Object(profile_obj));
|
||||
}
|
||||
}
|
||||
if !s.parent.is_empty() {
|
||||
spec_obj.insert("parent".to_string(), json!(&s.parent));
|
||||
}
|
||||
if !s.children.is_empty() {
|
||||
spec_obj.insert("children".to_string(), json!(&s.children));
|
||||
}
|
||||
if !s.members.is_empty() {
|
||||
spec_obj.insert("members".to_string(), json!(&s.members));
|
||||
}
|
||||
}
|
||||
entity::Spec::DomainSpec(s) => {
|
||||
if !s.owner.is_empty() {
|
||||
spec_obj.insert("owner".to_string(), json!(&s.owner));
|
||||
}
|
||||
}
|
||||
entity::Spec::ResourceSpec(s) => {
|
||||
if !s.r#type.is_empty() {
|
||||
spec_obj.insert("type".to_string(), json!(&s.r#type));
|
||||
}
|
||||
if !s.owner.is_empty() {
|
||||
spec_obj.insert("owner".to_string(), json!(&s.owner));
|
||||
}
|
||||
if !s.system.is_empty() {
|
||||
spec_obj.insert("system".to_string(), json!(&s.system));
|
||||
}
|
||||
if !s.depends_on.is_empty() {
|
||||
spec_obj.insert("depends_on".to_string(), json!(&s.depends_on));
|
||||
}
|
||||
}
|
||||
entity::Spec::DefectdojoSpec(_) => {
|
||||
// Plugin type — skip for now
|
||||
}
|
||||
entity::Spec::KeycloakSpec(_) => {
|
||||
// Plugin spec type — skip
|
||||
}
|
||||
entity::Spec::DependencytrackSpec(_) => {
|
||||
// Plugin spec type — skip
|
||||
}
|
||||
entity::Spec::FindingSpec(s) => {
|
||||
spec_obj.insert("component_ref".to_string(), json!(&s.component_ref));
|
||||
spec_obj.insert("lifecycle".to_string(), json!(&s.lifecycle));
|
||||
spec_obj.insert("scanner".to_string(), json!(&s.scanner));
|
||||
spec_obj.insert("rule_id".to_string(), json!(&s.rule_id));
|
||||
}
|
||||
}
|
||||
|
||||
entity_obj.insert("spec".to_string(), Value::Object(spec_obj));
|
||||
}
|
||||
|
||||
// Add annotations if present
|
||||
if !entity.annotations.is_empty() {
|
||||
entity_obj.insert("annotations".to_string(), json!(&entity.annotations));
|
||||
}
|
||||
|
||||
Ok(Value::Object(entity_obj))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::charybdis::core::ComponentMetadata;
|
||||
use crate::charybdis::entities::entity;
|
||||
use serde_json::json;
|
||||
|
||||
fn create_test_entity(id: &str, kind: &str) -> Entity {
|
||||
Entity {
|
||||
id: id.to_string(),
|
||||
kind: kind.to_string(),
|
||||
metadata: Some(entity::Metadata::ComponentMetadata(ComponentMetadata {
|
||||
name: "test-component".to_string(),
|
||||
description: "Test description".to_string(),
|
||||
..Default::default()
|
||||
})),
|
||||
spec: None,
|
||||
annotations: Default::default(),
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_direct_field_mapping() {
|
||||
let config = HashMap::from([
|
||||
("name".to_string(), json!("metadata.name")),
|
||||
("description".to_string(), json!("metadata.description")),
|
||||
]);
|
||||
|
||||
let mapper = FieldMapper::new(config).unwrap();
|
||||
let entity = create_test_entity("test-123", "Component");
|
||||
|
||||
let name = mapper.map("name", &entity).await.unwrap();
|
||||
assert_eq!(name, json!("test-component"));
|
||||
|
||||
let desc = mapper.map("description", &entity).await.unwrap();
|
||||
assert_eq!(desc, json!("Test description"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_static_field_mapping() {
|
||||
let config = HashMap::from([
|
||||
(
|
||||
"product_type".to_string(),
|
||||
json!({"value": "Web Application"}),
|
||||
),
|
||||
("is_active".to_string(), json!({"value": true})),
|
||||
("priority".to_string(), json!({"value": 100})),
|
||||
]);
|
||||
|
||||
let mapper = FieldMapper::new(config).unwrap();
|
||||
let entity = create_test_entity("test-123", "Component");
|
||||
|
||||
assert_eq!(
|
||||
mapper.map("product_type", &entity).await.unwrap(),
|
||||
json!("Web Application")
|
||||
);
|
||||
assert_eq!(mapper.map("is_active", &entity).await.unwrap(), json!(true));
|
||||
assert_eq!(mapper.map("priority", &entity).await.unwrap(), json!(100));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_map_all() {
|
||||
let config = HashMap::from([
|
||||
("name".to_string(), json!("metadata.name")),
|
||||
("type".to_string(), json!({"value": "Web Application"})),
|
||||
]);
|
||||
|
||||
let mapper = FieldMapper::new(config).unwrap();
|
||||
let entity = create_test_entity("test-123", "Component");
|
||||
|
||||
let mapped = mapper.map_all(&entity).await.unwrap();
|
||||
|
||||
assert_eq!(mapped.get("name").unwrap(), &json!("test-component"));
|
||||
assert_eq!(mapped.get("type").unwrap(), &json!("Web Application"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_complex_mapping_parse() {
|
||||
let config = HashMap::from([(
|
||||
"members".to_string(),
|
||||
json!({
|
||||
"from": "spec.owner",
|
||||
"resolve_entity": "Group",
|
||||
"extract": "spec.members"
|
||||
}),
|
||||
)]);
|
||||
|
||||
let mapper = FieldMapper::new(config).unwrap();
|
||||
assert!(mapper.has_mapping("members"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user