Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/functions/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub struct FunctionListQuery {
pub slug: Option<String>,
pub id: Option<String>,
pub version: Option<String>,
pub environment: Option<String>,
pub cursor: Option<String>,
pub snapshot: Option<String>,
}
Expand Down Expand Up @@ -116,11 +117,13 @@ pub async fn get_function_by_slug(
project_id: &str,
slug: &str,
version: Option<&str>,
environment: Option<&str>,
) -> Result<Option<Function>> {
let query = FunctionListQuery {
project_id: Some(project_id.to_string()),
slug: Some(slug.to_string()),
version: version.map(ToOwned::to_owned),
environment: environment.map(ToOwned::to_owned),
..Default::default()
};
let page = list_functions_page(client, &query).await?;
Expand All @@ -137,10 +140,12 @@ pub async fn get_function_by_id(
client: &ApiClient,
id: &str,
version: Option<&str>,
environment: Option<&str>,
) -> Result<Option<Function>> {
let query = FunctionListQuery {
id: Some(id.to_string()),
version: version.map(ToOwned::to_owned),
environment: environment.map(ToOwned::to_owned),
..Default::default()
};
let page = list_functions_page(client, &query).await?;
Expand Down Expand Up @@ -194,6 +199,9 @@ pub async fn list_functions_page(
if let Some(version) = &query.version {
params.push(("version", version.clone()));
}
if let Some(environment) = &query.environment {
params.push(("environment", environment.clone()));
}
if let Some(cursor) = &query.cursor {
params.push(("cursor", cursor.clone()));
}
Expand Down
2 changes: 1 addition & 1 deletion src/functions/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub async fn run(
let project_id = &ctx.project.id;

let function = match slug {
Some(s) => api::get_function_by_slug(&ctx.client, project_id, s, None)
Some(s) => api::get_function_by_slug(&ctx.client, project_id, s, None, None)
.await?
.ok_or_else(|| anyhow!("{} with slug '{s}' not found", label(ft)))?,
None => {
Expand Down
88 changes: 70 additions & 18 deletions src/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -438,9 +438,16 @@ pub struct ViewArgs {
/// Function id
#[arg(long = "id", env = "BT_FUNCTIONS_VIEW_ID")]
id: Option<String>,
/// Version selector.
#[arg(long, env = "BT_FUNCTIONS_VIEW_VERSION")]
/// Function version identifier (for example, a transaction ID)
#[arg(
long,
env = "BT_FUNCTIONS_VIEW_VERSION",
conflicts_with = "environment"
)]
version: Option<String>,
/// Environment slug whose assigned function version should be shown
#[arg(long, env = "BT_FUNCTIONS_VIEW_ENVIRONMENT")]
environment: Option<String>,
/// Open in browser
#[arg(long)]
web: bool,
Expand Down Expand Up @@ -629,10 +636,13 @@ pub(crate) async fn run_typed_command(
view::run_by_id(
&auth_ctx,
id,
v.version.as_deref(),
base.json,
v.web,
base.verbose,
view::ViewOptions {
version: v.version.as_deref(),
environment: v.environment.as_deref(),
json: base.json,
web: v.web,
verbose: base.verbose,
},
ft,
)
.await
Expand All @@ -642,10 +652,13 @@ pub(crate) async fn run_typed_command(
view::run(
&ctx,
slug,
v.version.as_deref(),
base.json,
v.web,
base.verbose,
view::ViewOptions {
version: v.version.as_deref(),
environment: v.environment.as_deref(),
json: base.json,
web: v.web,
verbose: base.verbose,
},
ft,
)
.await
Expand Down Expand Up @@ -684,10 +697,13 @@ pub async fn run(base: BaseArgs, args: FunctionsArgs) -> Result<()> {
view::run_by_id(
&auth_ctx,
id,
v.inner.version.as_deref(),
base.json,
v.inner.web,
base.verbose,
view::ViewOptions {
version: v.inner.version.as_deref(),
environment: v.inner.environment.as_deref(),
json: base.json,
web: v.inner.web,
verbose: base.verbose,
},
ft,
)
.await
Expand All @@ -697,10 +713,13 @@ pub async fn run(base: BaseArgs, args: FunctionsArgs) -> Result<()> {
view::run(
&ctx,
slug,
v.inner.version.as_deref(),
base.json,
v.inner.web,
base.verbose,
view::ViewOptions {
version: v.inner.version.as_deref(),
environment: v.inner.environment.as_deref(),
json: base.json,
web: v.inner.web,
verbose: base.verbose,
},
ft,
)
.await
Expand Down Expand Up @@ -1052,6 +1071,39 @@ mod tests {
assert_eq!(pull.slug_flag, vec!["a", "b", "c"]);
}

#[test]
fn view_accepts_environment_selector() {
let _guard = test_lock();
let parsed = parse(&[
"functions",
"view",
"test-function",
"--environment",
"production",
])
.expect("parse view");
let FunctionsCommands::View(view) = parsed.command.expect("subcommand") else {
panic!("expected view command");
};
assert_eq!(view.inner.environment.as_deref(), Some("production"));
}

#[test]
fn view_rejects_version_with_environment() {
let _guard = test_lock();
let err = parse(&[
"functions",
"view",
"test-function",
"--version",
"1234",
"--environment",
"production",
])
.expect_err("selectors should conflict");
assert!(err.to_string().contains("cannot be used with"));
}

#[test]
fn view_accepts_id_selector() {
let _guard = test_lock();
Expand Down
90 changes: 53 additions & 37 deletions src/functions/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,28 @@ use crate::{http::ApiClient, projects::api as projects_api};
use super::{api, build_web_path, label, label_plural, select_function_interactive};
use super::{AuthContext, FunctionTypeFilter, ResolvedContext};

#[derive(Debug, Clone, Copy)]
pub(crate) struct ViewOptions<'a> {
pub version: Option<&'a str>,
pub environment: Option<&'a str>,
pub json: bool,
pub web: bool,
pub verbose: bool,
}

pub async fn run(
ctx: &ResolvedContext,
slug: Option<&str>,
version: Option<&str>,
json: bool,
web: bool,
verbose: bool,
options: ViewOptions<'_>,
ft: Option<FunctionTypeFilter>,
) -> Result<()> {
let version = options.version;
let environment = options.environment;
let project_id = &ctx.project.id;
let function = match slug {
Some(s) => with_spinner(
&format!("Loading {}...", label(ft)),
api::get_function_by_slug(&ctx.client, project_id, s, version),
api::get_function_by_slug(&ctx.client, project_id, s, version, environment),
)
.await?
.ok_or_else(|| anyhow!("{} with slug '{s}' not found", label(ft)))?,
Expand All @@ -41,20 +49,27 @@ pub async fn run(
);
}
let selected = select_function_interactive(&ctx.client, project_id, ft).await?;
if let Some(version) = version {
if version.is_some() || environment.is_some() {
with_spinner(
&format!("Loading {}...", label(ft)),
api::get_function_by_slug(
&ctx.client,
project_id,
&selected.slug,
Some(version),
version,
environment,
),
)
.await?
.ok_or_else(|| {
let selector = version
.map(|version| format!("version {version}"))
.or_else(|| {
environment.map(|environment| format!("environment {environment}"))
})
.unwrap_or_default();
anyhow!(
"{} with slug '{}' not found at version {version}",
"{} with slug '{}' not found at {selector}",
label(ft),
selected.slug
)
Expand All @@ -70,51 +85,39 @@ pub async fn run(
&ctx.app_url,
Some(&ctx.project.name),
&function,
json,
web,
verbose,
options,
)
.await
}

pub async fn run_by_id(
ctx: &AuthContext,
id: &str,
version: Option<&str>,
json: bool,
web: bool,
verbose: bool,
options: ViewOptions<'_>,
ft: Option<FunctionTypeFilter>,
) -> Result<()> {
let version = options.version;
let environment = options.environment;
let function = with_spinner(
&format!("Loading {}...", label(ft)),
api::get_function_by_id(&ctx.client, id, version),
api::get_function_by_id(&ctx.client, id, version, environment),
)
.await?
.ok_or_else(|| anyhow!("{} with id '{id}' not found", label(ft)))?;

render_function(
&ctx.client,
&ctx.app_url,
None,
&function,
json,
web,
verbose,
)
.await
render_function(&ctx.client, &ctx.app_url, None, &function, options).await
}

async fn render_function(
client: &ApiClient,
app_url: &str,
project_name: Option<&str>,
function: &api::Function,
json: bool,
web: bool,
verbose: bool,
options: ViewOptions<'_>,
) -> Result<()> {
if web {
let requested_version = options.version;
let environment = options.environment;
if options.web {
let path = build_web_path(function);
let project_name = match project_name {
Some(project_name) => project_name.to_string(),
Expand All @@ -127,7 +130,7 @@ async fn render_function(
return Ok(());
}

if json {
if options.json {
println!("{}", serde_json::to_string(&function)?);
return Ok(());
}
Expand All @@ -140,6 +143,19 @@ async fn render_function(
console::style("Slug:").dim(),
function.slug
)?;
if let Some(environment) = environment {
writeln!(
output,
"{} {}",
console::style("Environment:").dim(),
environment
)?;
}
if requested_version.is_some() || environment.is_some() {
if let Some(version) = function._xact_id.as_deref().or(requested_version) {
writeln!(output, "{} {}", console::style("Version:").dim(), version)?;
}
}

if let Some(ft) = &function.function_type {
writeln!(output, "{} {}", console::style("Type:").dim(), ft)?;
Expand All @@ -151,15 +167,15 @@ async fn render_function(
}

if let Some(pd) = &function.prompt_data {
let options = pd.get("options");
if let Some(model) = options
let prompt_options = pd.get("options");
if let Some(model) = prompt_options
.and_then(|o| o.get("model"))
.and_then(|m| m.as_str())
{
writeln!(output, "{} {}", console::style("Model:").dim(), model)?;
}
if verbose {
if let Some(opts) = options {
if options.verbose {
if let Some(opts) = prompt_options {
render_options(&mut output, opts)?;
}
}
Expand Down Expand Up @@ -220,7 +236,7 @@ async fn render_function(
}
}

if verbose {
if options.verbose {
if let Some(bid) =
data.get("bundle_id").and_then(|b| b.as_str())
{
Expand Down Expand Up @@ -366,7 +382,7 @@ async fn render_function(
}
}

if verbose {
if options.verbose {
if let Some(tags) = &function.tags {
if !tags.is_empty() {
writeln!(
Expand Down
Loading
Loading