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 #[serde(default)]
58 pub use_azure_cli: bool,
59
60 pub entities: Vec<String>,
62
63 #[serde(default)]
65 pub entity_set_overrides: HashMap<String, String>,
66
67 #[serde(default)]
69 pub entity_columns: HashMap<String, Vec<String>>,
70
71 #[serde(default = "default_min_interval_ms")]
73 pub min_interval_ms: ConfigValue<u64>,
74
75 #[serde(default = "default_max_interval_seconds")]
78 pub max_interval_seconds: ConfigValue<u64>,
79
80 #[serde(default = "default_api_version")]
82 pub api_version: ConfigValue<String>,
83}
84
85#[derive(OpenApi)]
88#[openapi(components(schemas(DataverseSourceConfigDto)))]
89struct DataverseSourceSchemas;
90
91pub struct DataverseSourceDescriptor;
93
94#[async_trait]
95impl SourcePluginDescriptor for DataverseSourceDescriptor {
96 fn kind(&self) -> &str {
97 "dataverse"
98 }
99
100 fn config_version(&self) -> &str {
101 "1.0.0"
102 }
103
104 fn config_schema_name(&self) -> &str {
105 "source.dataverse.DataverseSourceConfig"
106 }
107
108 fn config_schema_json(&self) -> String {
109 let api = DataverseSourceSchemas::openapi();
110 serde_json::to_string(
111 &api.components
112 .as_ref()
113 .expect("OpenAPI components missing")
114 .schemas,
115 )
116 .expect("Failed to serialize config schema")
117 }
118
119 async fn create_source(
120 &self,
121 id: &str,
122 config_json: &serde_json::Value,
123 auto_start: bool,
124 ) -> anyhow::Result<Box<dyn drasi_lib::sources::Source>> {
125 let dto: DataverseSourceConfigDto = serde_json::from_value(config_json.clone())?;
126 let mapper = DtoMapper::new();
127
128 let environment_url = mapper.resolve_string(&dto.environment_url)?;
129 let tenant_id = mapper
130 .resolve_optional_string(&dto.tenant_id)?
131 .unwrap_or_default();
132 let client_id = mapper
133 .resolve_optional_string(&dto.client_id)?
134 .unwrap_or_default();
135 let client_secret = mapper
136 .resolve_optional_string(&dto.client_secret)?
137 .unwrap_or_default();
138 let min_interval_ms = mapper.resolve_typed(&dto.min_interval_ms)?;
139 let max_interval_seconds = mapper.resolve_typed(&dto.max_interval_seconds)?;
140 let api_version = mapper.resolve_string(&dto.api_version)?;
141
142 let config = DataverseSourceConfig {
143 environment_url,
144 tenant_id,
145 client_id,
146 client_secret,
147 use_azure_cli: dto.use_azure_cli,
148 entities: dto.entities,
149 entity_set_overrides: dto.entity_set_overrides,
150 entity_columns: dto.entity_columns,
151 min_interval_ms,
152 max_interval_seconds,
153 api_version,
154 };
155
156 let mut source = crate::DataverseSourceBuilder::new(id)
157 .with_environment_url(config.environment_url.clone())
158 .with_tenant_id(config.tenant_id.clone())
159 .with_client_id(config.client_id.clone())
160 .with_client_secret(config.client_secret.clone())
161 .with_entities(config.entities.clone())
162 .with_min_interval_ms(config.min_interval_ms)
163 .with_max_interval_seconds(config.max_interval_seconds)
164 .with_api_version(config.api_version.clone())
165 .with_auto_start(auto_start);
166
167 if config.use_azure_cli {
168 source = source.with_azure_cli_auth();
169 }
170
171 for (entity, set_name) in &config.entity_set_overrides {
173 source = source.with_entity_set_override(entity, set_name);
174 }
175
176 for (entity, columns) in &config.entity_columns {
178 source = source.with_entity_columns(entity, columns.clone());
179 }
180
181 let source = source.build()?;
182
183 Ok(Box::new(source))
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn test_dto_deserializes_from_camel_case_json() {
193 let json = serde_json::json!({
194 "environmentUrl": "https://myorg.crm.dynamics.com",
195 "tenantId": "tenant-1",
196 "clientId": "client-1",
197 "clientSecret": "secret-1",
198 "entities": ["account", "contact"]
199 });
200
201 let dto: DataverseSourceConfigDto =
202 serde_json::from_value(json).expect("should deserialize");
203
204 assert_eq!(
205 dto.environment_url,
206 ConfigValue::Static("https://myorg.crm.dynamics.com".to_string())
207 );
208 assert_eq!(
209 dto.tenant_id,
210 Some(ConfigValue::Static("tenant-1".to_string()))
211 );
212 assert_eq!(
213 dto.client_id,
214 Some(ConfigValue::Static("client-1".to_string()))
215 );
216 assert_eq!(
217 dto.client_secret,
218 Some(ConfigValue::Static("secret-1".to_string()))
219 );
220 assert_eq!(dto.entities, vec!["account", "contact"]);
221 }
222
223 #[test]
224 fn test_dto_applies_defaults() {
225 let json = serde_json::json!({
226 "environmentUrl": "https://myorg.crm.dynamics.com",
227 "entities": ["account"]
228 });
229
230 let dto: DataverseSourceConfigDto =
231 serde_json::from_value(json).expect("should deserialize");
232
233 assert_eq!(dto.min_interval_ms, ConfigValue::Static(500));
234 assert_eq!(dto.max_interval_seconds, ConfigValue::Static(30));
235 assert_eq!(dto.api_version, ConfigValue::Static("v9.2".to_string()));
236 assert!(!dto.use_azure_cli);
237 assert!(dto.entity_set_overrides.is_empty());
238 assert!(dto.entity_columns.is_empty());
239 assert_eq!(dto.tenant_id, None);
240 assert_eq!(dto.client_id, None);
241 assert_eq!(dto.client_secret, None);
242 }
243
244 #[test]
245 fn test_dto_azure_cli_mode() {
246 let json = serde_json::json!({
247 "environmentUrl": "https://myorg.crm.dynamics.com",
248 "useAzureCli": true,
249 "entities": ["account"]
250 });
251
252 let dto: DataverseSourceConfigDto =
253 serde_json::from_value(json).expect("should deserialize");
254
255 assert!(dto.use_azure_cli);
256 assert_eq!(dto.tenant_id, None);
257 }
258
259 #[test]
260 fn test_descriptor_metadata() {
261 let desc = DataverseSourceDescriptor;
262 assert_eq!(desc.kind(), "dataverse");
263 assert_eq!(desc.config_version(), "1.0.0");
264 assert_eq!(
265 desc.config_schema_name(),
266 "source.dataverse.DataverseSourceConfig"
267 );
268 let schema = desc.config_schema_json();
270 let _: serde_json::Value =
271 serde_json::from_str(&schema).expect("schema should be valid JSON");
272 }
273}