@@ -3,7 +3,7 @@ use wasmtime::Module;
33
44use std:: {
55 fs:: File ,
6- io:: { stdin, BufReader , Read } ,
6+ io:: { stdin, BufRead , BufReader , Read } ,
77 path:: PathBuf ,
88} ;
99
@@ -61,6 +61,14 @@ struct Opts {
6161 /// Path to graphql file containing Function input query; if omitted, defaults will be used to calculate limits.
6262 #[ clap( short = 'q' , long) ]
6363 query_path : Option < PathBuf > ,
64+
65+ /// Enable batch mode - read multiple JSON inputs (one per line) from stdin/file
66+ #[ clap( short, long) ]
67+ batch : bool ,
68+
69+ /// In batch mode, continue processing on individual input errors (default: false)
70+ #[ clap( long) ]
71+ batch_continue_on_error : bool ,
6472}
6573
6674impl Opts {
@@ -114,6 +122,31 @@ fn read_file_to_string(file_path: &PathBuf) -> Result<String> {
114122fn main ( ) -> Result < ( ) > {
115123 let opts: Opts = Opts :: parse ( ) ;
116124
125+ // Create engine and module once (expensive operations - amortize across all inputs)
126+ let engine = function_runner:: engine:: new_engine ( ) ?;
127+ let module = Module :: from_file ( & engine, & opts. function )
128+ . map_err ( |e| anyhow ! ( "Couldn't load the Function {:?}: {}" , & opts. function, e) ) ?;
129+
130+ // Infer codec from the module based on imported modules
131+ let codec = if function_runner:: engine:: uses_msgpack_provider ( & module) {
132+ Codec :: Messagepack
133+ } else {
134+ Codec :: Json
135+ } ;
136+
137+ if opts. batch {
138+ run_batch_mode ( & opts, & engine, & module, codec)
139+ } else {
140+ run_single_mode ( & opts, & engine, & module, codec)
141+ }
142+ }
143+
144+ fn run_single_mode (
145+ opts : & Opts ,
146+ engine : & wasmtime:: Engine ,
147+ module : & Module ,
148+ codec : Codec ,
149+ ) -> Result < ( ) > {
117150 let mut input: Box < dyn Read + Sync + Send + ' static > = if let Some ( ref input) = opts. input {
118151 Box :: new ( BufReader :: new ( File :: open ( input) . map_err ( |e| {
119152 anyhow ! ( "Couldn't load input {:?}: {}" , input, e)
@@ -130,20 +163,8 @@ fn main() -> Result<()> {
130163 input. read_to_end ( & mut buffer) ?;
131164
132165 let schema_string = opts. read_schema_to_string ( ) . transpose ( ) ?;
133-
134166 let query_string = opts. read_query_to_string ( ) . transpose ( ) ?;
135167
136- let engine = function_runner:: engine:: new_engine ( ) ?;
137- let module = Module :: from_file ( & engine, & opts. function )
138- . map_err ( |e| anyhow ! ( "Couldn't load the Function {:?}: {}" , & opts. function, e) ) ?;
139-
140- // Infer codec from the module based on imported modules
141- let codec = if function_runner:: engine:: uses_msgpack_provider ( & module) {
142- Codec :: Messagepack
143- } else {
144- Codec :: Json
145- } ;
146-
147168 let input = BytesContainer :: new ( BytesContainerType :: Input , codec, buffer) ?;
148169 let scale_factor = if let ( Some ( schema_string) , Some ( query_string) , Some ( json_value) ) =
149170 ( schema_string, query_string, input. json_value . clone ( ) )
@@ -156,19 +177,19 @@ fn main() -> Result<()> {
156177 & json_value,
157178 ) ?
158179 } else {
159- DEFAULT_SCALE_FACTOR // Use default scale factor when schema or query is missing
180+ DEFAULT_SCALE_FACTOR
160181 } ;
161182
162183 let profile_opts = opts. profile_opts ( ) ;
163184
164185 let function_run_result = run ( FunctionRunParams {
165- function_path : opts. function ,
186+ function_path : opts. function . clone ( ) ,
166187 input,
167188 export : opts. export . as_ref ( ) ,
168189 profile_opts : profile_opts. as_ref ( ) ,
169190 scale_factor,
170- module,
171- engine,
191+ module : module . clone ( ) ,
192+ engine : engine . clone ( ) ,
172193 } ) ?;
173194
174195 if opts. json {
@@ -187,3 +208,139 @@ fn main() -> Result<()> {
187208 anyhow:: bail!( "The Function execution failed. Review the logs for more information." )
188209 }
189210}
211+
212+ fn run_batch_mode (
213+ opts : & Opts ,
214+ engine : & wasmtime:: Engine ,
215+ module : & Module ,
216+ codec : Codec ,
217+ ) -> Result < ( ) > {
218+ let input_reader: Box < dyn BufRead > = if let Some ( ref input) = opts. input {
219+ Box :: new ( BufReader :: new ( File :: open ( input) . map_err ( |e| {
220+ anyhow ! ( "Couldn't load input {:?}: {}" , input, e)
221+ } ) ?) )
222+ } else if !std:: io:: stdin ( ) . is_terminal ( ) {
223+ Box :: new ( BufReader :: new ( stdin ( ) ) )
224+ } else {
225+ return Err ( anyhow ! (
226+ "You must provide input via the --input flag or piped via stdin."
227+ ) ) ;
228+ } ;
229+
230+ // Pre-calculate scale factor (constant across all inputs in batch)
231+ let schema_string = opts. read_schema_to_string ( ) . transpose ( ) ?;
232+ let query_string = opts. read_query_to_string ( ) . transpose ( ) ?;
233+
234+ // Disable profiling in batch mode for performance
235+ let profile_opts = None ;
236+
237+ let mut line_num = 0 ;
238+ let mut success_count = 0 ;
239+ let mut error_count = 0 ;
240+
241+ for line_result in input_reader. lines ( ) {
242+ line_num += 1 ;
243+
244+ let line = match line_result {
245+ Ok ( l) => l,
246+ Err ( e) => {
247+ error_count += 1 ;
248+ if opts. batch_continue_on_error {
249+ eprintln ! ( "Error reading line {}: {}" , line_num, e) ;
250+ println ! ( r#"{{"success":false,"error":"Error reading input: {}"}}"# , e) ;
251+ continue ;
252+ } else {
253+ return Err ( e. into ( ) ) ;
254+ }
255+ }
256+ } ;
257+
258+ // Skip empty lines
259+ if line. trim ( ) . is_empty ( ) {
260+ continue ;
261+ }
262+
263+ // Parse input
264+ let input = match BytesContainer :: new (
265+ BytesContainerType :: Input ,
266+ codec,
267+ line. into_bytes ( ) ,
268+ ) {
269+ Ok ( i) => i,
270+ Err ( e) => {
271+ error_count += 1 ;
272+ if opts. batch_continue_on_error {
273+ eprintln ! ( "Error parsing line {}: {}" , line_num, e) ;
274+ println ! ( r#"{{"success":false,"error":"Invalid JSON input: {}"}}"# , e) ;
275+ continue ;
276+ } else {
277+ return Err ( e) ;
278+ }
279+ }
280+ } ;
281+
282+ // Calculate scale factor for this input
283+ let scale_factor = if let ( Some ( ref schema_string) , Some ( ref query_string) , Some ( ref json_value) ) =
284+ ( & schema_string, & query_string, & input. json_value )
285+ {
286+ match BluejaySchemaAnalyzer :: analyze_schema_definition (
287+ schema_string,
288+ opts. schema_path . as_ref ( ) . and_then ( |p| p. to_str ( ) ) ,
289+ query_string,
290+ opts. query_path . as_ref ( ) . and_then ( |p| p. to_str ( ) ) ,
291+ json_value,
292+ ) {
293+ Ok ( sf) => sf,
294+ Err ( e) => {
295+ error_count += 1 ;
296+ if opts. batch_continue_on_error {
297+ eprintln ! ( "Error analyzing schema for line {}: {}" , line_num, e) ;
298+ println ! ( r#"{{"success":false,"error":"Schema analysis failed: {}"}}"# , e) ;
299+ continue ;
300+ } else {
301+ return Err ( e) ;
302+ }
303+ }
304+ }
305+ } else {
306+ DEFAULT_SCALE_FACTOR
307+ } ;
308+
309+ // Run function (reusing engine/module!)
310+ let result = run ( FunctionRunParams {
311+ function_path : opts. function . clone ( ) ,
312+ input,
313+ export : opts. export . as_ref ( ) ,
314+ profile_opts,
315+ scale_factor,
316+ module : module. clone ( ) ,
317+ engine : engine. clone ( ) ,
318+ } ) ;
319+
320+ // Output result immediately (streaming JSONL - compact format for line-by-line parsing)
321+ match result {
322+ Ok ( function_result) => {
323+ success_count += 1 ;
324+ // Use compact JSON (not pretty-printed) for JSONL format
325+ let compact_json = serde_json:: to_string ( & function_result)
326+ . unwrap_or_else ( |error| error. to_string ( ) ) ;
327+ println ! ( "{}" , compact_json) ;
328+ }
329+ Err ( e) => {
330+ error_count += 1 ;
331+ if opts. batch_continue_on_error {
332+ eprintln ! ( "Error executing line {}: {}" , line_num, e) ;
333+ println ! ( r#"{{"success":false,"error":"Execution failed: {}"}}"# , e) ;
334+ } else {
335+ return Err ( e) ;
336+ }
337+ }
338+ }
339+ }
340+
341+ // Log summary to stderr (so it doesn't interfere with JSONL output on stdout)
342+ eprintln ! ( "Batch complete: {} inputs processed, {} successful, {} errors" ,
343+ line_num, success_count, error_count) ;
344+
345+ Ok ( ( ) )
346+ }
0 commit comments