Skip to content

Commit 158f844

Browse files
committed
refactor(functions push): tighten manifest path
- Drop redundant is_object() pre-check (validator handles it). - Pass entry project_id/name as raw Option<&str>; resolve_project_id already trims and filters empty. - Use take() on args.manifest instead of clone(). - expect("validated above") instead of if-let-Some on as_object_mut. - entry().or_insert_with for if_exists default. - Trim doc comments and verbose assertion messages; the spec is the body.
1 parent 7d73bf3 commit 158f844

3 files changed

Lines changed: 63 additions & 140 deletions

File tree

src/functions/mod.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -270,10 +270,8 @@ pub(crate) struct PushArgs {
270270
)]
271271
pub file_flag: Vec<PathBuf>,
272272

273-
/// JSON manifest file with one or more raw function definitions.
274-
/// Skips the SDK runner; the manifest is posted directly to /insert-functions.
275-
/// Use this for function types the SDK doesn't expose builders for
276-
/// (preprocessor with inline-quickjs runtime, facet, classifier).
273+
/// Push raw JSON function definitions, skipping the SDK runner. Use for
274+
/// types with no SDK builder (facet, classifier, inline-quickjs preprocessor).
277275
#[arg(
278276
long = "manifest",
279277
env = "BT_FUNCTIONS_PUSH_MANIFEST",

src/functions/push.rs

Lines changed: 49 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,8 @@ impl ClassifiedFiles {
191191
}
192192
}
193193

194-
pub async fn run(base: BaseArgs, args: PushArgs) -> Result<()> {
195-
if let Some(manifest_path) = args.manifest.clone() {
194+
pub async fn run(base: BaseArgs, mut args: PushArgs) -> Result<()> {
195+
if let Some(manifest_path) = args.manifest.take() {
196196
return run_manifest_push(base, args, manifest_path).await;
197197
}
198198

@@ -692,21 +692,15 @@ struct FileSuccess {
692692
bundle_id: Option<String>,
693693
}
694694

695-
/// Push a raw JSON manifest of function definitions, skipping the SDK runner.
696-
///
697-
/// Use case: function types that the Braintrust SDK doesn't expose builders
698-
/// for (preprocessor with inline-quickjs runtime, facet, classifier). The
699-
/// manifest body is posted directly to /insert-functions, the same endpoint
700-
/// the SDK-runner path uses after scraping registrations.
695+
/// Push function definitions from a raw JSON manifest, skipping the SDK runner.
696+
/// Used for function types the SDK has no builder for (facet, classifier,
697+
/// inline-quickjs preprocessor).
701698
async fn run_manifest_push(
702699
base: BaseArgs,
703700
args: PushArgs,
704701
manifest_path: PathBuf,
705702
) -> Result<()> {
706-
let auth_ctx = match resolve_auth_context(&base)
707-
.await
708-
.context("failed to resolve auth context")
709-
{
703+
let auth_ctx = match resolve_auth_context(&base).await {
710704
Ok(ctx) => ctx,
711705
Err(err) => {
712706
return fail_push(
@@ -745,9 +739,9 @@ async fn run_manifest_push(
745739
}
746740
};
747741

748-
let entries: Vec<Value> = match parsed {
742+
let mut entries: Vec<Value> = match parsed {
749743
Value::Array(arr) => arr,
750-
Value::Object(_) => vec![parsed],
744+
obj @ Value::Object(_) => vec![obj],
751745
_ => {
752746
return fail_push(
753747
&base,
@@ -769,46 +763,29 @@ async fn run_manifest_push(
769763
);
770764
}
771765

766+
// `-p` is a project name by convention (see resolve_project_context_optional);
767+
// used as fallback for entries that don't carry their own project_id / name.
772768
let mut project_name_cache = BTreeMap::new();
773-
let mut function_events: Vec<Value> = Vec::with_capacity(entries.len());
774-
775-
// Resolve the CLI `-p` flag once. The flag is treated as a project name
776-
// (the convention shared by every other `bt` subcommand — see
777-
// resolve_project_context_optional in functions/mod.rs). Used as fallback
778-
// when a manifest entry doesn't carry its own project_id / project_name.
779-
let cli_project_default_id = match super::resolve_project_context_optional(
780-
&base, &auth_ctx, false,
781-
)
782-
.await
783-
{
784-
Ok(Some(project)) => {
785-
project_name_cache.insert(project.name.clone(), project.id.clone());
786-
Some(project.id)
787-
}
788-
Ok(None) => None,
789-
Err(err) => {
790-
return fail_push(
791-
&base,
792-
0,
793-
HardFailureReason::ManifestSchemaInvalid,
794-
format!("failed to resolve --project: {err:#}"),
795-
"failed to resolve CLI project flag",
796-
);
797-
}
798-
};
799-
800-
for (index, mut entry) in entries.into_iter().enumerate() {
801-
if !entry.is_object() {
802-
return fail_push(
803-
&base,
804-
0,
805-
HardFailureReason::ManifestSchemaInvalid,
806-
format!("manifest entry {index} is not a JSON object"),
807-
"manifest entry must be a JSON object",
808-
);
809-
}
769+
let cli_project_default_id =
770+
match super::resolve_project_context_optional(&base, &auth_ctx, false).await {
771+
Ok(Some(project)) => {
772+
project_name_cache.insert(project.name.clone(), project.id.clone());
773+
Some(project.id)
774+
}
775+
Ok(None) => None,
776+
Err(err) => {
777+
return fail_push(
778+
&base,
779+
0,
780+
HardFailureReason::ManifestSchemaInvalid,
781+
format!("failed to resolve --project: {err:#}"),
782+
"failed to resolve CLI project flag",
783+
);
784+
}
785+
};
810786

811-
if let Err(err) = validate_manifest_entry(&entry, index) {
787+
for (index, entry) in entries.iter_mut().enumerate() {
788+
if let Err(err) = validate_manifest_entry(entry, index) {
812789
return fail_push(
813790
&base,
814791
0,
@@ -818,23 +795,11 @@ async fn run_manifest_push(
818795
);
819796
}
820797

821-
let entry_project_id = entry
822-
.get("project_id")
823-
.and_then(Value::as_str)
824-
.map(str::trim)
825-
.filter(|value| !value.is_empty())
826-
.map(ToOwned::to_owned);
827-
let entry_project_name = entry
828-
.get("project_name")
829-
.and_then(Value::as_str)
830-
.map(str::trim)
831-
.filter(|value| !value.is_empty())
832-
.map(ToOwned::to_owned);
833-
let resolved_project_id = match resolve_project_id(
798+
let project_id = match resolve_project_id(
834799
&auth_ctx.client,
835800
cli_project_default_id.as_deref(),
836-
entry_project_id.as_deref(),
837-
entry_project_name.as_deref(),
801+
entry.get("project_id").and_then(Value::as_str),
802+
entry.get("project_name").and_then(Value::as_str),
838803
&mut project_name_cache,
839804
args.create_missing_projects,
840805
)
@@ -852,25 +817,18 @@ async fn run_manifest_push(
852817
}
853818
};
854819

855-
if let Some(object) = entry.as_object_mut() {
856-
object.insert("project_id".to_string(), Value::String(resolved_project_id));
857-
object.remove("project_name");
858-
if object.get("if_exists").is_none() {
859-
object.insert(
860-
"if_exists".to_string(),
861-
Value::String(args.if_exists.as_str().to_string()),
862-
);
863-
}
864-
}
865-
866-
function_events.push(entry);
820+
let object = entry.as_object_mut().expect("validated as object above");
821+
object.insert("project_id".to_string(), Value::String(project_id));
822+
object.remove("project_name");
823+
object
824+
.entry("if_exists".to_string())
825+
.or_insert_with(|| Value::String(args.if_exists.as_str().to_string()));
867826
}
868827

869828
if !args.yes && is_interactive() {
870-
let count = function_events.len();
871829
let prompt = format!(
872-
"Push {count} manifest entr{} from {} to {}?",
873-
if count == 1 { "y" } else { "ies" },
830+
"Push {} entries from {} to {}?",
831+
entries.len(),
874832
manifest_path.display(),
875833
current_org_label(&auth_ctx),
876834
);
@@ -879,12 +837,11 @@ async fn run_manifest_push(
879837
.default(false)
880838
.interact()?;
881839
if !confirmed {
882-
return cancel_push(&base, &[manifest_path.clone()]);
840+
return cancel_push(&base, &[manifest_path]);
883841
}
884842
}
885843

886-
let total_entries = function_events.len();
887-
let insert_result = match api::insert_functions(&auth_ctx.client, &function_events).await {
844+
let insert_result = match api::insert_functions(&auth_ctx.client, &entries).await {
888845
Ok(result) => result,
889846
Err(err) => {
890847
return fail_push(
@@ -901,7 +858,7 @@ async fn run_manifest_push(
901858
};
902859

903860
let (uploaded_entries, ignored_entries) =
904-
calculate_upload_counts(total_entries, insert_result.ignored_entries);
861+
calculate_upload_counts(entries.len(), insert_result.ignored_entries);
905862

906863
let summary = PushSummary {
907864
status: CommandStatus::Success,
@@ -923,14 +880,11 @@ async fn run_manifest_push(
923880
errors: vec![],
924881
};
925882

926-
let use_progress =
927-
!base.json && std::io::stderr().is_terminal() && animations_enabled() && !is_quiet();
928-
if use_progress {
883+
if !base.json && std::io::stderr().is_terminal() && animations_enabled() && !is_quiet() {
929884
eprintln!(
930-
"{} Successfully pushed {} entr{} from {}",
885+
"{} Pushed {} entries from {}",
931886
style("✓").green(),
932887
uploaded_entries,
933-
if uploaded_entries == 1 { "y" } else { "ies" },
934888
manifest_path.display(),
935889
);
936890
}
@@ -939,28 +893,22 @@ async fn run_manifest_push(
939893
Ok(())
940894
}
941895

942-
/// Validate the minimum required fields on a manifest entry.
943-
///
944-
/// Required: `name`, `slug`, `function_type`, `function_data` (object).
945-
/// Project may come from `project_id` / `project_name` on the entry, or
946-
/// fall back to the CLI's `-p` flag.
947896
fn validate_manifest_entry(entry: &Value, index: usize) -> Result<()> {
948897
let object = entry
949898
.as_object()
950899
.ok_or_else(|| anyhow!("entry {index} is not a JSON object"))?;
951900

952901
for required in ["name", "slug", "function_type"] {
953-
let value = object
902+
let present = object
954903
.get(required)
955904
.and_then(Value::as_str)
956-
.map(str::trim)
957-
.filter(|value| !value.is_empty());
958-
if value.is_none() {
905+
.is_some_and(|value| !value.trim().is_empty());
906+
if !present {
959907
bail!("entry {index} is missing required string field '{required}'");
960908
}
961909
}
962910

963-
if !object.get("function_data").map_or(false, Value::is_object) {
911+
if !object.get("function_data").is_some_and(Value::is_object) {
964912
bail!("entry {index} is missing required object field 'function_data'");
965913
}
966914

tests/functions.rs

Lines changed: 12 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1843,68 +1843,45 @@ async fn functions_push_manifest_works_against_mock_api() {
18431843
let summary: Value = serde_json::from_slice(&output.stdout).expect("parse push summary");
18441844
assert_eq!(summary["status"].as_str(), Some("success"));
18451845
assert_eq!(summary["uploaded_files"].as_u64(), Some(1));
1846-
assert_eq!(summary["failed_files"].as_u64(), Some(0));
18471846

18481847
let inserted = state
18491848
.inserted_functions
18501849
.lock()
18511850
.expect("inserted functions lock")
18521851
.clone();
1853-
assert_eq!(
1854-
inserted.len(),
1855-
1,
1856-
"exactly one function should be inserted"
1857-
);
1852+
assert_eq!(inserted.len(), 1);
18581853
let first = inserted[0].as_object().expect("inserted function object");
18591854
assert_eq!(
18601855
first.get("project_id").and_then(Value::as_str),
1861-
Some("proj_mock"),
1862-
"project_id should be resolved from -p flag"
1863-
);
1864-
assert_eq!(
1865-
first.get("slug").and_then(Value::as_str),
1866-
Some("mock-facet-v1")
1856+
Some("proj_mock")
18671857
);
18681858
assert_eq!(
18691859
first.get("function_type").and_then(Value::as_str),
18701860
Some("facet")
18711861
);
18721862
assert_eq!(
18731863
first.get("if_exists").and_then(Value::as_str),
1874-
Some("replace"),
1875-
"if_exists from --if-exists should be applied to manifest entries"
1864+
Some("replace")
18761865
);
1877-
let function_data = first
1878-
.get("function_data")
1879-
.and_then(Value::as_object)
1880-
.expect("function_data object");
1866+
// The point of the patch: facet-shaped function_data passes through verbatim,
1867+
// including fields the SDK has no builder for (preprocessor reference).
18811868
assert_eq!(
1882-
function_data.get("type").and_then(Value::as_str),
1883-
Some("facet"),
1884-
"facet manifest function_data.type should pass through unchanged"
1869+
first.get("function_data").and_then(|v| v.get("type")),
1870+
manifest_body.get("function_data").and_then(|v| v.get("type"))
18851871
);
18861872
assert_eq!(
1887-
function_data.get("no_match_pattern").and_then(Value::as_str),
1888-
Some("^NONE"),
1889-
"facet-specific fields should pass through verbatim"
1890-
);
1891-
assert!(
1892-
function_data
1893-
.get("preprocessor")
1894-
.and_then(Value::as_object)
1895-
.is_some(),
1896-
"preprocessor reference should pass through"
1873+
first.get("function_data").and_then(|v| v.get("preprocessor")),
1874+
manifest_body
1875+
.get("function_data")
1876+
.and_then(|v| v.get("preprocessor"))
18971877
);
18981878

18991879
let uploaded = state
19001880
.uploaded_bundles
19011881
.lock()
19021882
.expect("uploaded bundles lock")
19031883
.clone();
1904-
assert!(
1905-
uploaded.is_empty(),
1906-
"manifest push must not upload a bundle"
1907-
);
1884+
assert!(uploaded.is_empty(), "manifest push must not upload a bundle");
19081885
}
19091886

19101887
#[cfg(unix)]

0 commit comments

Comments
 (0)