1use faucet_core::FaucetError;
9use faucet_core::discover::{DatasetDescriptor, columns_to_schema, nullable_type};
10use quick_xml::Reader;
11use quick_xml::events::{BytesStart, Event};
12use serde_json::{Value, json};
13use std::collections::HashMap;
14
15#[derive(Debug, Clone, PartialEq)]
17pub struct EdmProperty {
18 pub name: String,
20 pub edm_type: String,
22 pub nullable: bool,
24}
25
26#[derive(Debug, Clone, PartialEq, Default)]
28pub struct EdmEntityType {
29 pub name: String,
31 pub keys: Vec<String>,
33 pub properties: Vec<EdmProperty>,
35}
36
37#[derive(Debug, Clone, PartialEq)]
39pub struct EdmEntitySet {
40 pub name: String,
42 pub type_name: String,
44}
45
46pub fn edm_type_to_json(edm_type: &str) -> Value {
48 let t = edm_type.strip_prefix("Edm.").unwrap_or(edm_type);
49 let ty = match t {
50 "Boolean" => "boolean",
51 "Byte" | "SByte" | "Int16" | "Int32" | "Int64" => "integer",
52 "Decimal" | "Double" | "Single" => "number",
53 _ => "string",
56 };
57 json!({ "type": ty })
58}
59
60fn local_name(qname: &[u8]) -> String {
62 let s = String::from_utf8_lossy(qname);
63 s.rsplit(':').next().unwrap_or(&s).to_string()
64}
65
66fn attr(e: &BytesStart, key: &str) -> Option<String> {
68 e.attributes()
69 .flatten()
70 .find(|a| local_name(a.key.as_ref()) == key)
71 .and_then(|a| a.unescape_value().ok().map(|v| v.to_string()))
72}
73
74pub fn parse_edmx(xml: &str) -> Result<(Vec<EdmEntityType>, Vec<EdmEntitySet>), FaucetError> {
76 let mut reader = Reader::from_str(xml);
77 let mut types: Vec<EdmEntityType> = Vec::new();
78 let mut sets: Vec<EdmEntitySet> = Vec::new();
79 let mut current: Option<EdmEntityType> = None;
80 let mut in_key = false;
81
82 let open = |e: &BytesStart,
85 is_empty: bool,
86 current: &mut Option<EdmEntityType>,
87 in_key: &mut bool,
88 types: &mut Vec<EdmEntityType>,
89 sets: &mut Vec<EdmEntitySet>| {
90 match local_name(e.name().as_ref()).as_str() {
91 "EntityType" => {
92 let t = EdmEntityType {
93 name: attr(e, "Name").unwrap_or_default(),
94 ..Default::default()
95 };
96 if is_empty {
97 types.push(t);
98 } else {
99 *current = Some(t);
100 }
101 }
102 "Key" => {
103 if !is_empty {
104 *in_key = true;
105 }
106 }
107 "PropertyRef" => {
108 if *in_key && let (Some(cur), Some(n)) = (current.as_mut(), attr(e, "Name")) {
109 cur.keys.push(n);
110 }
111 }
112 "Property" => {
113 if let Some(cur) = current.as_mut() {
114 let name = attr(e, "Name").unwrap_or_default();
115 if !name.is_empty() {
116 cur.properties.push(EdmProperty {
117 name,
118 edm_type: attr(e, "Type").unwrap_or_else(|| "Edm.String".to_owned()),
119 nullable: attr(e, "Nullable").map(|v| v != "false").unwrap_or(true),
121 });
122 }
123 }
124 }
125 "EntitySet" => {
126 if let (Some(name), Some(ty)) = (attr(e, "Name"), attr(e, "EntityType")) {
127 let type_name = ty.rsplit('.').next().unwrap_or(&ty).to_owned();
128 sets.push(EdmEntitySet { name, type_name });
129 }
130 }
131 _ => {}
132 }
133 };
134
135 loop {
136 match reader
137 .read_event()
138 .map_err(|e| FaucetError::Source(format!("odata: invalid $metadata XML: {e}")))?
139 {
140 Event::Eof => break,
141 Event::Start(e) => open(&e, false, &mut current, &mut in_key, &mut types, &mut sets),
142 Event::Empty(e) => open(&e, true, &mut current, &mut in_key, &mut types, &mut sets),
143 Event::End(e) => match local_name(e.name().as_ref()).as_str() {
144 "EntityType" => {
145 if let Some(t) = current.take() {
146 types.push(t);
147 }
148 }
149 "Key" => in_key = false,
150 _ => {}
151 },
152 _ => {}
153 }
154 }
155 Ok((types, sets))
156}
157
158pub fn descriptors_from_edmx(xml: &str) -> Result<Vec<DatasetDescriptor>, FaucetError> {
161 let (types, sets) = parse_edmx(xml)?;
162 let by_name: HashMap<&str, &EdmEntityType> =
163 types.iter().map(|t| (t.name.as_str(), t)).collect();
164 let mut out = Vec::with_capacity(sets.len());
165 for set in &sets {
166 let schema = by_name.get(set.type_name.as_str()).map(|t| {
167 let cols = t.properties.iter().map(|p| {
168 let frag = edm_type_to_json(&p.edm_type);
169 let frag = if p.nullable {
170 nullable_type(frag)
171 } else {
172 frag
173 };
174 (p.name.clone(), frag)
175 });
176 columns_to_schema(cols)
177 });
178 let mut d = DatasetDescriptor::new(
179 set.name.clone(),
180 "entity",
181 json!({ "odata": { "entity": set.name.clone() } }),
182 );
183 if let Some(s) = schema {
184 d = d.with_schema(s);
185 }
186 out.push(d);
187 }
188 Ok(out)
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 const SAMPLE: &str = r#"<?xml version="1.0" encoding="utf-8"?>
196<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
197 <edmx:DataServices>
198 <Schema Namespace="Sales" xmlns="http://docs.oasis-open.org/odata/ns/edm">
199 <EntityType Name="Order">
200 <Key><PropertyRef Name="DocEntry"/></Key>
201 <Property Name="DocEntry" Type="Edm.Int32" Nullable="false"/>
202 <Property Name="DocDate" Type="Edm.DateTimeOffset"/>
203 <Property Name="Total" Type="Edm.Decimal" Nullable="false"/>
204 <Property Name="Posted" Type="Edm.Boolean"/>
205 </EntityType>
206 <EntityContainer Name="Container">
207 <EntitySet Name="Orders" EntityType="Sales.Order"/>
208 </EntityContainer>
209 </Schema>
210 </edmx:DataServices>
211</edmx:Edmx>"#;
212
213 #[test]
214 fn edm_types_map_to_json_types() {
215 assert_eq!(edm_type_to_json("Edm.Boolean"), json!({"type": "boolean"}));
216 assert_eq!(edm_type_to_json("Edm.Int64"), json!({"type": "integer"}));
217 assert_eq!(edm_type_to_json("Edm.Decimal"), json!({"type": "number"}));
218 assert_eq!(edm_type_to_json("Edm.String"), json!({"type": "string"}));
219 assert_eq!(
220 edm_type_to_json("Edm.DateTimeOffset"),
221 json!({"type": "string"})
222 );
223 assert_eq!(
224 edm_type_to_json("Something.Custom"),
225 json!({"type": "string"})
226 );
227 }
228
229 #[test]
230 fn parse_edmx_extracts_types_and_sets() {
231 let (types, sets) = parse_edmx(SAMPLE).unwrap();
232 assert_eq!(types.len(), 1);
233 assert_eq!(types[0].name, "Order");
234 assert_eq!(types[0].keys, vec!["DocEntry".to_string()]);
235 assert_eq!(types[0].properties.len(), 4);
236 assert_eq!(types[0].properties[0].name, "DocEntry");
237 assert!(!types[0].properties[0].nullable);
238 assert!(types[0].properties[1].nullable); assert_eq!(sets.len(), 1);
240 assert_eq!(sets[0].name, "Orders");
241 assert_eq!(sets[0].type_name, "Order");
242 }
243
244 #[test]
245 fn descriptors_carry_schema_and_config_patch() {
246 let ds = descriptors_from_edmx(SAMPLE).unwrap();
247 assert_eq!(ds.len(), 1);
248 let d = &ds[0];
249 assert_eq!(d.name, "Orders");
250 assert_eq!(d.kind, "entity");
251 assert_eq!(d.config_patch, json!({ "odata": { "entity": "Orders" } }));
252 let schema = d.schema.as_ref().unwrap();
253 assert_eq!(schema["type"], "object");
254 assert_eq!(schema["properties"]["DocEntry"]["type"], "integer");
255 assert_eq!(schema["properties"]["Total"]["type"], "number");
256 assert_eq!(schema["properties"]["Posted"]["type"][0], "boolean");
257 assert_eq!(schema["properties"]["Posted"]["type"][1], "null");
258 }
259
260 #[test]
261 fn empty_entity_type_and_missing_type_are_tolerated() {
262 let xml = r#"<Schema>
263 <EntityType Name="Empty"/>
264 <EntitySet Name="Ghosts" EntityType="ns.NotDeclared"/>
265 </Schema>"#;
266 let ds = descriptors_from_edmx(xml).unwrap();
267 assert_eq!(ds.len(), 1);
268 assert!(ds[0].schema.is_none());
270 assert_eq!(ds[0].name, "Ghosts");
271 }
272
273 #[test]
274 fn invalid_xml_errors() {
275 assert!(parse_edmx("<Schema><EntityType Name=").is_err());
276 }
277
278 #[test]
279 fn property_without_name_is_skipped() {
280 let xml = r#"<Schema>
283 <EntityType Name="T">
284 <Property Type="Edm.String"/>
285 <Property Name="ok" Type="Edm.String" Nullable="true"/>
286 </EntityType>
287 </Schema>"#;
288 let (types, _sets) = parse_edmx(xml).unwrap();
289 assert_eq!(types[0].properties.len(), 1);
290 assert_eq!(types[0].properties[0].name, "ok");
291 assert!(types[0].properties[0].nullable);
292 }
293}