1use crate::prelude::*;
2use serde_json::{json, Map, Value as JsonValue};
3use std::collections::{HashMap, HashSet};
4
5pub struct BatchMetadataBuilder {
7 ops_metadata: Vec<OpMetadata>,
8}
9
10impl BatchMetadataBuilder {
11 pub fn new<T>(ops: &[Arc<dyn Op<T>>]) -> Self {
12 Self {
13 ops_metadata: ops.iter().map(|op| op.metadata()).collect(),
14 }
15 }
16
17 pub fn build(&self) -> OpMetadata {
19 let (input_schema, outputs_by_index) = self.analyze_input_requirements();
20 let reference_schema = self.merge_reference_schemas();
21 let output_schema = self.construct_output_schema(&outputs_by_index);
22
23 OpMetadata::builder("BatchOp")
24 .description(format!(
25 "Batch of {} operations with data flow analysis",
26 self.ops_metadata.len()
27 ))
28 .input_schema(input_schema)
29 .reference_schema(reference_schema)
30 .output_schema(output_schema)
31 .build()
32 }
33
34 fn analyze_input_requirements(&self) -> (JsonValue, HashMap<usize, HashSet<String>>) {
37 let mut required_inputs = HashSet::new();
38 let mut available_outputs = HashSet::new();
39 let mut outputs_by_index = HashMap::new();
40
41 for (index, metadata) in self.ops_metadata.iter().enumerate() {
43 if let Some(input_schema) = &metadata.input_schema {
45 if let Some(required) = self.extract_required_fields(input_schema) {
46 for field in required {
47 if !available_outputs.contains(&field) {
49 required_inputs.insert(field);
50 }
51 }
52 }
53 }
54
55 let op_outputs = self.extract_output_fields(&metadata.output_schema);
57 outputs_by_index.insert(index, op_outputs.clone());
58 available_outputs.extend(op_outputs);
59 }
60
61 let schema = self.build_input_schema_from_requirements(required_inputs);
63 (schema, outputs_by_index)
64 }
65
66 fn extract_required_fields(&self, schema: &JsonValue) -> Option<Vec<String>> {
68 schema
69 .as_object()
70 .and_then(|obj| obj.get("required"))
71 .and_then(|req| req.as_array())
72 .map(|arr| {
73 arr.iter()
74 .filter_map(|v| v.as_str())
75 .map(|s| s.to_string())
76 .collect()
77 })
78 }
79
80 fn extract_output_fields(&self, schema: &Option<JsonValue>) -> HashSet<String> {
82 let mut fields = HashSet::new();
83
84 if let Some(schema) = schema {
85 if let Some(obj) = schema.as_object() {
87 if let Some(properties) = obj.get("properties").and_then(|p| p.as_object()) {
89 fields.extend(properties.keys().cloned());
90 } else if obj.get("type").and_then(|t| t.as_str()) == Some("string") {
91 fields.insert("result".to_string());
94 }
95 }
96 }
97
98 fields
99 }
100
101 fn build_input_schema_from_requirements(&self, required_fields: HashSet<String>) -> JsonValue {
103 let mut properties = Map::new();
104 let mut required = Vec::new();
105
106 for metadata in &self.ops_metadata {
108 if let Some(input_schema) = &metadata.input_schema {
109 if let Some(schema_props) = input_schema
110 .as_object()
111 .and_then(|obj| obj.get("properties"))
112 .and_then(|p| p.as_object())
113 {
114 for (field_name, field_schema) in schema_props {
115 if required_fields.contains(field_name)
116 && !properties.contains_key(field_name)
117 {
118 properties.insert(field_name.clone(), field_schema.clone());
119 required.push(JsonValue::String(field_name.clone()));
120 }
121 }
122 }
123 }
124 }
125
126 json!({
127 "type": "object",
128 "properties": properties,
129 "required": required,
130 "additionalProperties": false
131 })
132 }
133
134 fn merge_reference_schemas(&self) -> JsonValue {
136 let mut all_properties = Map::new();
137 let mut all_required = HashSet::new();
138
139 for metadata in &self.ops_metadata {
140 if let Some(ref_schema) = &metadata.reference_schema {
141 if let Some(obj) = ref_schema.as_object() {
142 if let Some(properties) = obj.get("properties").and_then(|p| p.as_object()) {
144 for (key, value) in properties {
145 if !all_properties.contains_key(key) {
146 all_properties.insert(key.clone(), value.clone());
147 }
148 }
149 }
150
151 if let Some(required) = obj.get("required").and_then(|r| r.as_array()) {
153 for req in required {
154 if let Some(field) = req.as_str() {
155 all_required.insert(field.to_string());
156 }
157 }
158 }
159 }
160 }
161 }
162
163 let required: Vec<JsonValue> = all_required.into_iter().map(JsonValue::String).collect();
164
165 json!({
166 "type": "object",
167 "properties": all_properties,
168 "required": required,
169 "additionalProperties": false
170 })
171 }
172
173 fn construct_output_schema(
175 &self,
176 _outputs_by_index: &HashMap<usize, HashSet<String>>,
177 ) -> JsonValue {
178 json!({
182 "type": "array",
183 "items": {
184 "type": "object",
185 "description": "Output from individual ops in the batch"
186 },
187 "minItems": self.ops_metadata.len(),
188 "maxItems": self.ops_metadata.len()
189 })
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 struct ProducerOp;
198 struct ConsumerOp;
199
200 #[async_trait]
201 impl Op<String> for ProducerOp {
202 async fn perform(&self, dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<String> {
203 let input = dry.get_required::<String>("initial_input")?;
204 Ok(format!("produced_{}", input))
205 }
206
207 fn metadata(&self) -> OpMetadata {
208 OpMetadata::builder("ProducerOp")
209 .input_schema(json!({
210 "type": "object",
211 "properties": {
212 "initial_input": { "type": "string" }
213 },
214 "required": ["initial_input"]
215 }))
216 .output_schema(json!({
217 "type": "object",
218 "properties": {
219 "produced_value": { "type": "string" }
220 }
221 }))
222 .build()
223 }
224 }
225
226 #[async_trait]
227 impl Op<String> for ConsumerOp {
228 async fn perform(&self, dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<String> {
229 let value = dry.get_required::<String>("produced_value")?;
230 Ok(format!("consumed_{}", value))
231 }
232
233 fn metadata(&self) -> OpMetadata {
234 OpMetadata::builder("ConsumerOp")
235 .input_schema(json!({
236 "type": "object",
237 "properties": {
238 "produced_value": { "type": "string" }
239 },
240 "required": ["produced_value"]
241 }))
242 .output_schema(json!({
243 "type": "object",
244 "properties": {
245 "final_result": { "type": "string" }
246 }
247 }))
248 .build()
249 }
250 }
251
252 #[test]
254 fn test0047_batch_metadata_with_data_flow() {
255 let ops: Vec<Arc<dyn Op<String>>> = vec![Arc::new(ProducerOp), Arc::new(ConsumerOp)];
256
257 let builder = BatchMetadataBuilder::new(&ops);
258 let metadata = builder.build();
259
260 if let Some(input_schema) = metadata.input_schema {
263 let required = input_schema
264 .get("required")
265 .and_then(|r| r.as_array())
266 .unwrap();
267 assert_eq!(required.len(), 1);
268 assert_eq!(required[0].as_str().unwrap(), "initial_input");
269 }
270 }
271
272 #[test]
274 fn test0048_reference_schema_merging() {
275 struct ServiceAOp;
276 struct ServiceBOp;
277
278 #[async_trait]
279 impl Op<()> for ServiceAOp {
280 async fn perform(&self, _dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<()> {
281 Ok(())
282 }
283
284 fn metadata(&self) -> OpMetadata {
285 OpMetadata::builder("ServiceAOp")
286 .reference_schema(json!({
287 "type": "object",
288 "properties": {
289 "service_a": { "type": "ServiceA" }
290 },
291 "required": ["service_a"]
292 }))
293 .build()
294 }
295 }
296
297 #[async_trait]
298 impl Op<()> for ServiceBOp {
299 async fn perform(&self, _dry: &mut DryContext, _wet: &mut WetContext) -> OpResult<()> {
300 Ok(())
301 }
302
303 fn metadata(&self) -> OpMetadata {
304 OpMetadata::builder("ServiceBOp")
305 .reference_schema(json!({
306 "type": "object",
307 "properties": {
308 "service_b": { "type": "ServiceB" }
309 },
310 "required": ["service_b"]
311 }))
312 .build()
313 }
314 }
315
316 let ops: Vec<Arc<dyn Op<()>>> = vec![Arc::new(ServiceAOp), Arc::new(ServiceBOp)];
317
318 let builder = BatchMetadataBuilder::new(&ops);
319 let metadata = builder.build();
320
321 if let Some(ref_schema) = metadata.reference_schema {
323 let required = ref_schema
324 .get("required")
325 .and_then(|r| r.as_array())
326 .unwrap();
327 assert_eq!(required.len(), 2);
328
329 let required_strs: Vec<&str> = required.iter().filter_map(|v| v.as_str()).collect();
330 assert!(required_strs.contains(&"service_a"));
331 assert!(required_strs.contains(&"service_b"));
332 }
333 }
334}