Skip to content

Commit 3445ef1

Browse files
committed
feat(cli): Allow referencing json files when submitting
The `submit` subcommand now accepts JSON array (old behavior) OR JSON array file prefixed with `@` OR `--` followed by individual parameters encoded as JSON, supporting `@`-prefixed files as well.
1 parent 4f3849e commit 3445ef1

4 files changed

Lines changed: 92 additions & 16 deletions

File tree

crates/executor/src/executor.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,6 @@ impl<C: ClockFn + 'static> ExecTask<C> {
331331
}
332332
}
333333

334-
// FIXME: On a slow execution: race between `expired_timers_watcher` this if retry_exp_backoff is 0.
335334
/// Map the `WorkerError` to an temporary or a permanent failure.
336335
fn worker_result_to_execution_event(
337336
execution_id: ExecutionId,

src/args.rs

Lines changed: 88 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,8 @@ pub(crate) enum Execution {
136136
/// Submit new execution and optionally follow its status stream until the it finishes.
137137
Submit {
138138
/// Function in the fully qualified format
139-
#[arg(value_name = "FUNCTION")]
139+
#[arg(value_name = "function")]
140140
ffqn: FunctionFqn,
141-
/// Parameters encoded as an JSON
142-
#[arg(value_name = "PARAMS")]
143-
params: String,
144141
/// Follow the stream of events until the execution finishes
145142
#[arg(short, long)]
146143
follow: bool,
@@ -149,9 +146,21 @@ pub(crate) enum Execution {
149146
no_reconnect: bool,
150147
/// Print output as JSON
151148
#[arg(long)]
152-
json: bool,
149+
json: bool, // TODO: output=json|jsonl|plain
150+
/// Parameters for the function. Accepts one of the following formats:
151+
///
152+
/// - A single argument containing a JSON array string (e.g., '["a", "b"]')
153+
///
154+
/// - A single argument prefixed with '@' referencing a file that contains the JSON array
155+
///
156+
/// - Multiple individual arguments following '--' (e.g., -- "a" @secondparam.json null 1)
157+
#[expect(clippy::doc_link_with_quotes)] // Intentional use of '
158+
#[arg(name = "parameters")]
159+
params: Vec<String>,
153160
},
161+
/// Write a return value or an execution error to an already created stubbed execution.
154162
Stub(Stub),
163+
/// Get the current state of an execution.
155164
Get {
156165
/// Follow the status stream until the execution finishes.
157166
#[arg(short, long)]
@@ -169,6 +178,80 @@ pub(crate) enum Execution {
169178
},
170179
}
171180

181+
pub(crate) mod params {
182+
use clap::error::ErrorKind;
183+
use serde_json::Value;
184+
185+
pub(crate) fn parse_params(params: Vec<String>) -> Result<Vec<u8>, clap::Error> {
186+
if params.is_empty() {
187+
Ok("[]".to_string().into_bytes()) // no params, does not matter if `--` was present.
188+
} else if params.len() == 1 && !dashdash() {
189+
let mut params = params;
190+
let json_array = params.pop().expect("checked that len == 1");
191+
// Single JSON Array, or a `@`-prefixed file containing the array.
192+
let json_array = if let Some(file_path) = json_array.strip_prefix('@') {
193+
std::fs::read_to_string(file_path).map_err(|err| {
194+
clap::Error::raw(
195+
ErrorKind::Io,
196+
format!(
197+
"parameter parsing failed: failed to read file '{file_path}': {err}"
198+
),
199+
)
200+
})?
201+
} else {
202+
json_array
203+
};
204+
let json_value = serde_json::from_str(&json_array).map_err(|err| {
205+
clap::Error::raw(
206+
ErrorKind::ValueValidation,
207+
format!("Invalid JSON array for parameters: {err}"),
208+
)
209+
})?;
210+
let Value::Array(_) = &json_value else {
211+
return Err(clap::Error::raw(
212+
ErrorKind::ValueValidation,
213+
"Parameter provided as JSON must be a JSON array.",
214+
));
215+
};
216+
Ok(json_value.to_string().into_bytes())
217+
} else {
218+
// Fallback to raw arguments. Each argument is interpreted as a JSON value or a file starting with `@` that contains the JSON.
219+
let mut parsed_params: Vec<Value> = Vec::new();
220+
for (idx, arg) in params.into_iter().enumerate() {
221+
let arg = if let Some(file_path) = arg.strip_prefix('@') {
222+
std::fs::read_to_string(file_path).map_err(|err| {
223+
clap::Error::raw(
224+
ErrorKind::Io,
225+
format!(
226+
"{idx}-th parameter parsing failed: failed to read file '{file_path}': {err}"
227+
),
228+
)
229+
})?
230+
} else {
231+
arg
232+
};
233+
let json = serde_json::from_str(&arg).map_err(|err| {
234+
clap::Error::raw(
235+
ErrorKind::ValueValidation,
236+
format!("{idx}-th parameter parsing failed: cannot parse as JSON: {err}"),
237+
)
238+
})?;
239+
parsed_params.push(json);
240+
}
241+
Ok(Value::Array(parsed_params).to_string().into_bytes())
242+
}
243+
}
244+
245+
fn dashdash() -> bool {
246+
// Ambigous: Either the single JSON Array representing all parameters,
247+
// OR `-- "first-and-only-json-param"`
248+
let mut rev_arg_iter = std::env::args().rev();
249+
rev_arg_iter.next().expect("last arg must be present");
250+
let maybe_separator = rev_arg_iter.next().expect("last-1 arg must be present");
251+
maybe_separator == "--"
252+
}
253+
}
254+
172255
#[derive(Debug, clap::Args)]
173256
#[command(group(
174257
ArgGroup::new("result")

src/command/execution.rs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ use crate::grpc_util::grpc_gen;
44
use crate::grpc_util::grpc_gen::execution_status::BlockedByJoinSet;
55
use crate::grpc_util::grpc_gen::execution_status::Finished;
66
use anyhow::Context as _;
7-
use anyhow::bail;
87
use chrono::DateTime;
98
use concepts::JOIN_SET_ID_INFIX;
109
use concepts::JoinSetKind;
@@ -26,22 +25,17 @@ pub(crate) enum SubmitOutputOpts {
2625
pub(crate) async fn submit(
2726
mut client: ExecutionRepositoryClient,
2827
ffqn: FunctionFqn,
29-
params: String,
28+
params: Vec<u8>,
3029
follow: bool,
3130
opts: SubmitOutputOpts,
3231
) -> anyhow::Result<()> {
3332
let execution_id = ExecutionId::generate();
34-
// Verify params are string parseable as JSON array.
35-
match serde_json::from_str(&params).context("PARAMS must be a JSON-encoded string")? {
36-
serde_json::Value::Array(_) => {}
37-
_ => bail!("PARAMS must be a JSON-encoded array"),
38-
}
3933
client
4034
.submit(tonic::Request::new(grpc_gen::SubmitRequest {
4135
execution_id: Some(execution_id.clone().into()),
4236
params: Some(prost_wkt_types::Any {
4337
type_url: format!("urn:obelisk:json:params:{ffqn}"),
44-
value: params.into_bytes(),
38+
value: params,
4539
}),
4640
function_name: Some(ffqn.into()),
4741
}))

src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ mod grpc_util;
66
mod init;
77
mod oci;
88

9-
use args::{Args, Client, ClientSubcommand, Generate, Server, Subcommand};
9+
use args::{Args, Client, ClientSubcommand, Generate, Server, Subcommand, params::parse_params};
1010
use clap::Parser;
1111
use command::{
1212
execution::{GetStatusOptions, SubmitOutputOpts},
@@ -116,7 +116,7 @@ async fn main() -> Result<(), anyhow::Error> {
116116
} else {
117117
SubmitOutputOpts::PlainFollow { no_reconnect }
118118
};
119-
command::execution::submit(client, ffqn, params, follow, opts).await
119+
command::execution::submit(client, ffqn, parse_params(params)?, follow, opts).await
120120
}
121121
ClientSubcommand::Execution(args::Execution::Stub(args::Stub {
122122
execution_id,

0 commit comments

Comments
 (0)