Public Access
730 lines
28 KiB
Rust
730 lines
28 KiB
Rust
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.keys() {
|
|
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"));
|
|
}
|
|
}
|