use crate::api::rest_operation::RestOperation;
use crate::auth::Authenticator;
use crate::client::ForceClient;
use crate::error::Result;
use std::collections::HashMap;
use std::fmt::Write;
use super::scanner::FieldUsageScanner;
use super::schema_analyzer::analyze_schema;
use super::schema_graph::SchemaGraph;
pub async fn generate_visualizer_report<A: Authenticator>(
client: &ForceClient<A>,
sobject: &str,
include_usage: bool,
) -> Result<String> {
let describe = client.rest().describe(sobject).await?;
let insights = analyze_schema(&describe);
let mut md = String::with_capacity(2048);
let _ = writeln!(md, "# Schema Report: {}\n", describe.label);
let _ = writeln!(md, "**API Name:** `{}`", describe.name);
let _ = writeln!(md, "**Custom:** {}\n", describe.custom);
let _ = writeln!(md, "## Schema Insights\n");
let _ = writeln!(
md,
"* **Complexity Score:** {}",
insights.complexity_score
);
let _ = writeln!(md, "* **Total Fields:** {}", insights.total_fields);
let _ = writeln!(
md,
"* **Standard Fields:** {}",
insights.standard_field_count
);
let _ = writeln!(md, "* **Custom Fields:** {}", insights.custom_field_count);
let _ = writeln!(
md,
"* **Required Fields:** {}\n",
insights.required_field_count
);
let fields_for_usage = if include_usage {
Some(describe.fields.clone())
} else {
None
};
let mut graph = SchemaGraph::new(client);
graph.add_describe(describe);
let _ = writeln!(md, "## Entity-Relationship Diagram\n");
let _ = writeln!(md, "```mermaid");
graph.write_mermaid(&mut md);
let _ = writeln!(md, "```\n");
if include_usage {
let scanner = FieldUsageScanner::new(client);
let usages = scanner.scan(sobject).await?;
let mut usage_map = HashMap::with_capacity(usages.len());
for usage in usages {
usage_map.insert(usage.name, usage.percentage);
}
let _ = writeln!(md, "## Field Usage Statistics\n");
let _ = writeln!(md, "| Label | API Name | Populated % |");
let _ = writeln!(md, "|---|---|---|");
let mut fields = fields_for_usage.unwrap_or_default();
fields.sort_by(|a, b| crate::schema::cmp_field_names(&a.name, &b.name));
for field in &fields {
if let Some(pct) = usage_map.get(&field.name) {
let _ = writeln!(md, "| {} | `{}` | {:.1}% |", field.label, field.name, pct);
} else {
let _ = writeln!(md, "| {} | `{}` | N/A |", field.label, field.name);
}
}
}
Ok(md)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::builder;
use crate::test_support::{MockAuthenticator, Must};
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
async fn create_mock_server() -> MockServer {
MockServer::start().await
}
async fn create_test_client(mock_server: &MockServer) -> ForceClient<MockAuthenticator> {
let auth = MockAuthenticator::new("test_token", &mock_server.uri());
builder().authenticate(auth).build().await.must()
}
#[tokio::test]
async fn test_schema_visualizer_generate_report() {
let mock_server = create_mock_server().await;
let client = create_test_client(&mock_server).await;
let describe_json = serde_json::from_str::<serde_json::Value>(r#"{
"name": "Account",
"label": "Account",
"custom": false,
"queryable": true,
"activateable": false, "createable": true, "customSetting": false, "deletable": true,
"deprecatedAndHidden": false, "feedEnabled": true, "hasSubtypes": false,
"isSubtype": false, "keyPrefix": "001", "labelPlural": "Accounts", "layoutable": true,
"mergeable": true, "mruEnabled": true, "replicateable": true, "retrieveable": true,
"searchable": true, "triggerable": true, "undeletable": true, "updateable": true,
"urls": {}, "childRelationships": [], "recordTypeInfos": [],
"fields": [
{
"name": "Id", "type": "id", "label": "Account ID", "createable": false,
"autoNumber": false, "calculated": false, "custom": false, "nillable": false,
"defaultedOnCreate": true, "referenceTo": [],
"aggregatable": true, "byteLength": 18,
"cascadeDelete": false, "caseSensitive": false,
"dependentPicklist": false, "deprecatedAndHidden": false,
"digits": 0, "displayLocationInDecimal": false, "encrypted": false, "externalId": false,
"filterable": true, "groupable": true, "highScaleNumber": false, "htmlFormatted": false,
"idLookup": true, "length": 18, "nameField": false, "namePointing": false,
"permissionable": false, "polymorphicForeignKey": false, "precision": 0, "queryByDistance": false,
"restrictedDelete": false, "restrictedPicklist": false, "scale": 0, "soapType": "xsd:id",
"sortable": true, "unique": true, "updateable": false, "writeRequiresMasterRead": false
},
{
"name": "Name", "type": "string", "label": "Account Name", "createable": true,
"autoNumber": false, "calculated": false, "custom": false, "nillable": false,
"defaultedOnCreate": false, "referenceTo": [],
"aggregatable": true, "byteLength": 255,
"cascadeDelete": false, "caseSensitive": false,
"dependentPicklist": false, "deprecatedAndHidden": false,
"digits": 0, "displayLocationInDecimal": false, "encrypted": false, "externalId": false,
"filterable": true, "groupable": true, "highScaleNumber": false, "htmlFormatted": false,
"idLookup": false, "length": 255, "nameField": true, "namePointing": false,
"permissionable": false, "polymorphicForeignKey": false, "precision": 0, "queryByDistance": false,
"restrictedDelete": false, "restrictedPicklist": false, "scale": 0, "soapType": "xsd:string",
"sortable": true, "unique": false, "updateable": true, "writeRequiresMasterRead": false
}
]
}"#).must();
Mock::given(method("GET"))
.and(path("/services/data/v60.0/sobjects/Account/describe"))
.respond_with(ResponseTemplate::new(200).set_body_json(describe_json))
.mount(&mock_server)
.await;
let md = generate_visualizer_report(&client, "Account", false)
.await
.must();
assert!(md.contains("# Schema Report: Account"));
assert!(md.contains("**API Name:** `Account`"));
assert!(md.contains("## Schema Insights"));
assert!(md.contains("## Entity-Relationship Diagram"));
assert!(md.contains("```mermaid"));
assert!(md.contains("erDiagram"));
assert!(!md.contains("## Field Usage Statistics"));
}
}