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;
#[derive(Debug)]
pub struct DataDictionary<'a, A: Authenticator> {
client: &'a ForceClient<A>,
}
impl<'a, A: Authenticator> DataDictionary<'a, A> {
#[must_use]
pub fn new(client: &'a ForceClient<A>) -> Self {
Self { client }
}
pub async fn generate(&self, sobject: &str, include_usage: bool) -> Result<String> {
let describe = self.client.rest().describe(sobject).await?;
let mut usage_map = HashMap::new();
if include_usage {
let scanner = FieldUsageScanner::new(self.client);
let usages = scanner.scan(sobject).await?;
usage_map.reserve(usages.len());
for usage in usages {
usage_map.insert(usage.name, usage.percentage);
}
}
let mut md = String::with_capacity(1024);
let _ = writeln!(md, "# Data Dictionary: {}", describe.label);
let _ = writeln!(md, "**API Name:** `{}`", describe.name);
let _ = writeln!(md, "**Custom:** {}", describe.custom);
md.push('\n');
md.push_str("## Fields\n\n");
if include_usage {
md.push_str("| Label | API Name | Type | Required | Reference To | Populated % |\n");
md.push_str("|---|---|---|---|---|---|\n");
} else {
md.push_str("| Label | API Name | Type | Required | Reference To |\n");
md.push_str("|---|---|---|---|---|\n");
}
let mut fields = describe.fields;
fields.sort_by(|a, b| crate::schema::cmp_field_names(&a.name, &b.name));
for field in fields {
let required = if !field.nillable && !field.defaulted_on_create {
"Yes"
} else {
"No"
};
let _ = write!(
md,
"| {} | `{}` | {:?} | {} | ",
field.label, field.name, field.type_, required
);
let mut first = true;
for r in &field.reference_to {
if !first {
md.push_str(", ");
}
md.push_str(r);
first = false;
}
if include_usage {
if let Some(&percentage) = usage_map.get(&field.name) {
let _ = writeln!(md, " | {:.1}% |", percentage);
} else {
md.push_str(" | N/A |\n");
}
} else {
md.push_str(" |\n");
}
}
Ok(md)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::builder;
use crate::test_support::{MockAuthenticator, Must};
use serde_json::json;
use wiremock::matchers::{method, path, query_param};
use wiremock::{Mock, MockServer, ResponseTemplate};
#[tokio::test]
async fn test_generate_dictionary_without_usage() {
let mock_server = MockServer::start().await;
let auth = MockAuthenticator::new("token", &mock_server.uri());
let client = builder().authenticate(auth).build().await.must();
let id_field = json!({
"name": "Id", "type": "id", "label": "Account ID", "nillable": false,
"defaultedOnCreate": true, "referenceTo": [],
"aggregatable": true, "autoNumber": false, "byteLength": 18, "calculated": false,
"cascadeDelete": false, "caseSensitive": false, "createable": false, "custom": 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": "tns:ID", "sortable": true, "unique": false, "updateable": false,
"writeRequiresMasterRead": false
});
let name_field = json!({
"name": "Name", "type": "string", "label": "Account Name", "nillable": false,
"defaultedOnCreate": false, "referenceTo": ["Account", "Contact"],
"aggregatable": true, "autoNumber": false, "byteLength": 255, "calculated": false,
"cascadeDelete": false, "caseSensitive": false, "createable": true, "custom": 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
});
let describe_json = json!({
"name": "Account", "label": "Account", "custom": true, "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": [id_field, name_field]
});
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 dict = DataDictionary::new(&client);
let md = dict.generate("Account", false).await.must();
assert!(md.contains("# Data Dictionary: Account"));
assert!(md.contains("**Custom:** true"));
assert!(!md.contains("Populated %"));
assert!(md.contains("| Label | API Name | Type | Required | Reference To |"));
assert!(md.contains("Account, Contact"));
}
#[tokio::test]
async fn test_generate_dictionary() {
let mock_server = MockServer::start().await;
let auth = MockAuthenticator::new("token", &mock_server.uri());
let client = builder().authenticate(auth).build().await.must();
let id_field = json!({
"name": "Id", "type": "id", "label": "Account ID", "nillable": false,
"defaultedOnCreate": true, "referenceTo": [],
"aggregatable": true, "autoNumber": false, "byteLength": 18, "calculated": false,
"cascadeDelete": false, "caseSensitive": false, "createable": false, "custom": 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": "tns:ID", "sortable": true, "unique": false, "updateable": false,
"writeRequiresMasterRead": false
});
let name_field = json!({
"name": "Name", "type": "string", "label": "Account Name", "nillable": false,
"defaultedOnCreate": false, "referenceTo": [],
"aggregatable": true, "autoNumber": false, "byteLength": 255, "calculated": false,
"cascadeDelete": false, "caseSensitive": false, "createable": true, "custom": 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
});
let parent_id_field = json!({
"name": "ParentId", "type": "reference", "label": "Parent Account ID", "nillable": true,
"defaultedOnCreate": false, "referenceTo": ["Account"],
"aggregatable": true, "autoNumber": false, "byteLength": 18, "calculated": false,
"cascadeDelete": false, "caseSensitive": false, "createable": true, "custom": false,
"dependentPicklist": false, "deprecatedAndHidden": false, "digits": 0,
"displayLocationInDecimal": false, "encrypted": false, "externalId": false,
"filterable": true, "groupable": true, "highScaleNumber": false, "htmlFormatted": false,
"idLookup": false, "length": 18, "nameField": false, "namePointing": false,
"permissionable": false, "polymorphicForeignKey": false, "precision": 0,
"queryByDistance": false, "restrictedDelete": false, "restrictedPicklist": false,
"scale": 0, "soapType": "tns:ID", "sortable": true, "unique": false, "updateable": true,
"writeRequiresMasterRead": false
});
let describe_json = json!({
"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": [id_field, name_field, parent_id_field]
});
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 query_json = json!({
"totalSize": 1, "done": true,
"records": [{
"attributes": { "type": "AggregateResult", "url": "..." },
"total": 100, "f0": 100, "f1": 95, "f2": 25
}]
});
Mock::given(method("GET"))
.and(path("/services/data/v60.0/query"))
.and(query_param("q", "SELECT COUNT(Id) total, COUNT(Id) f0, COUNT(Name) f1, COUNT(ParentId) f2 FROM Account"))
.respond_with(ResponseTemplate::new(200).set_body_json(query_json))
.mount(&mock_server)
.await;
let dict = DataDictionary::new(&client);
let md = dict.generate("Account", true).await.must();
assert!(md.contains("# Data Dictionary: Account"));
assert!(md.contains("**API Name:** `Account`"));
assert!(md.contains("| Account ID | `Id` | Id | No | | 100.0% |"));
assert!(md.contains("| Account Name | `Name` | String | Yes | | 95.0% |"));
assert!(
md.contains("| Parent Account ID | `ParentId` | Reference | No | Account | 25.0% |")
);
}
}