drasi_source_dataverse/
descriptor.rs1use crate::DataverseSourceConfig;
18use drasi_plugin_sdk::prelude::*;
19use std::collections::HashMap;
20use utoipa::OpenApi;
21
22fn default_min_interval_ms() -> ConfigValue<u64> {
25 ConfigValue::Static(500)
26}
27
28fn default_max_interval_seconds() -> ConfigValue<u64> {
29 ConfigValue::Static(30)
30}
31
32fn default_api_version() -> ConfigValue<String> {
33 ConfigValue::Static("v9.2".to_string())
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
38#[schema(as = source::dataverse::DataverseSourceConfig)]
39#[serde(rename_all = "camelCase", deny_unknown_fields)]
40pub struct DataverseSourceConfigDto {
41 pub environment_url: ConfigValue<String>,
43
44 #[serde(default)]
46 pub tenant_id: Option<ConfigValue<String>>,
47
48 #[serde(default)]
50 pub client_id: Option<ConfigValue<String>>,
51
52 #[serde(default)]
54 pub client_secret: Option<ConfigValue<String>>,
55
56 pub entities: Vec<String>,
58
59 #[serde(default)]
61 pub entity_set_overrides: HashMap<String, String>,
62
63 #[serde(default)]
65 pub entity_columns: HashMap<String, Vec<String>>,
66
67 #[serde(default = "default_min_interval_ms")]
69 pub min_interval_ms: ConfigValue<u64>,
70
71 #[serde(default = "default_max_interval_seconds")]
74 pub max_interval_seconds: ConfigValue<u64>,
75
76 #[serde(default = "default_api_version")]
78 pub api_version: ConfigValue<String>,
79}
80
81#[derive(OpenApi)]
84#[openapi(components(schemas(DataverseSourceConfigDto)))]
85struct DataverseSourceSchemas;
86
87pub struct DataverseSourceDescriptor;
89
90#[async_trait]
91impl SourcePluginDescriptor for DataverseSourceDescriptor {
92 fn kind(&self) -> &str {
93 "dataverse"
94 }
95
96 fn config_version(&self) -> &str {
97 "1.0.0"
98 }
99
100 fn config_schema_name(&self) -> &str {
101 "source.dataverse.DataverseSourceConfig"
102 }
103
104 fn config_schema_json(&self) -> String {
105 let api = DataverseSourceSchemas::openapi();
106 serde_json::to_string(
107 &api.components
108 .as_ref()
109 .expect("OpenAPI components missing")
110 .schemas,
111 )
112 .expect("Failed to serialize config schema")
113 }
114
115 async fn create_source(
116 &self,
117 id: &str,
118 config_json: &serde_json::Value,
119 auto_start: bool,
120 ) -> anyhow::Result<Box<dyn drasi_lib::sources::Source>> {
121 let dto: DataverseSourceConfigDto = serde_json::from_value(config_json.clone())?;
122 let mapper = DtoMapper::new();
123
124 let environment_url = mapper.resolve_string(&dto.environment_url).await?;
125 let tenant_id = mapper
126 .resolve_optional_string(&dto.tenant_id)
127 .await?
128 .unwrap_or_default();
129 let client_id = mapper
130 .resolve_optional_string(&dto.client_id)
131 .await?
132 .unwrap_or_default();
133 let client_secret = mapper
134 .resolve_optional_string(&dto.client_secret)
135 .await?
136 .unwrap_or_default();
137 let min_interval_ms = mapper.resolve_typed(&dto.min_interval_ms).await?;
138 let max_interval_seconds = mapper.resolve_typed(&dto.max_interval_seconds).await?;
139 let api_version = mapper.resolve_string(&dto.api_version).await?;
140
141 let config = DataverseSourceConfig {
142 environment_url,
143 tenant_id,
144 client_id,
145 client_secret,
146 entities: dto.entities,
147 entity_set_overrides: dto.entity_set_overrides,
148 entity_columns: dto.entity_columns,
149 min_interval_ms,
150 max_interval_seconds,
151 api_version,
152 };
153
154 let mut source = crate::DataverseSourceBuilder::new(id)
155 .with_environment_url(config.environment_url.clone())
156 .with_tenant_id(config.tenant_id.clone())
157 .with_client_id(config.client_id.clone())
158 .with_client_secret(config.client_secret.clone())
159 .with_entities(config.entities.clone())
160 .with_min_interval_ms(config.min_interval_ms)
161 .with_max_interval_seconds(config.max_interval_seconds)
162 .with_api_version(config.api_version.clone())
163 .with_auto_start(auto_start);
164
165 for (entity, set_name) in &config.entity_set_overrides {
167 source = source.with_entity_set_override(entity, set_name);
168 }
169
170 for (entity, columns) in &config.entity_columns {
172 source = source.with_entity_columns(entity, columns.clone());
173 }
174
175 let source = source.build()?;
176
177 Ok(Box::new(source))
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn test_dto_deserializes_from_camel_case_json() {
187 let json = serde_json::json!({
188 "environmentUrl": "https://myorg.crm.dynamics.com",
189 "tenantId": "tenant-1",
190 "clientId": "client-1",
191 "clientSecret": "secret-1",
192 "entities": ["account", "contact"]
193 });
194
195 let dto: DataverseSourceConfigDto =
196 serde_json::from_value(json).expect("should deserialize");
197
198 assert_eq!(
199 dto.environment_url,
200 ConfigValue::Static("https://myorg.crm.dynamics.com".to_string())
201 );
202 assert_eq!(
203 dto.tenant_id,
204 Some(ConfigValue::Static("tenant-1".to_string()))
205 );
206 assert_eq!(
207 dto.client_id,
208 Some(ConfigValue::Static("client-1".to_string()))
209 );
210 assert_eq!(
211 dto.client_secret,
212 Some(ConfigValue::Static("secret-1".to_string()))
213 );
214 assert_eq!(dto.entities, vec!["account", "contact"]);
215 }
216
217 #[test]
218 fn test_dto_applies_defaults() {
219 let json = serde_json::json!({
220 "environmentUrl": "https://myorg.crm.dynamics.com",
221 "entities": ["account"]
222 });
223
224 let dto: DataverseSourceConfigDto =
225 serde_json::from_value(json).expect("should deserialize");
226
227 assert_eq!(dto.min_interval_ms, ConfigValue::Static(500));
228 assert_eq!(dto.max_interval_seconds, ConfigValue::Static(30));
229 assert_eq!(dto.api_version, ConfigValue::Static("v9.2".to_string()));
230 assert!(dto.entity_set_overrides.is_empty());
231 assert!(dto.entity_columns.is_empty());
232 assert_eq!(dto.tenant_id, None);
233 assert_eq!(dto.client_id, None);
234 assert_eq!(dto.client_secret, None);
235 }
236
237 #[test]
238 fn test_descriptor_metadata() {
239 let desc = DataverseSourceDescriptor;
240 assert_eq!(desc.kind(), "dataverse");
241 assert_eq!(desc.config_version(), "1.0.0");
242 assert_eq!(
243 desc.config_schema_name(),
244 "source.dataverse.DataverseSourceConfig"
245 );
246 let schema = desc.config_schema_json();
248 let _: serde_json::Value =
249 serde_json::from_str(&schema).expect("schema should be valid JSON");
250 }
251}