diff --git a/crates/plasm-runtime/src/execution/compile_preflight.rs b/crates/plasm-runtime/src/execution/compile_preflight.rs index 41f4eb9f..c8c82dcb 100644 --- a/crates/plasm-runtime/src/execution/compile_preflight.rs +++ b/crates/plasm-runtime/src/execution/compile_preflight.rs @@ -138,7 +138,7 @@ fn preflight_compile_create( message: e.to_string(), } })?; - apply_preflight_compile_stubs(&mut env, capability); + apply_preflight_compile_stubs(&mut env, capability, cgs); merge_plasm_execute_session_env(&mut env); compile_operation_dispatch(&capability_template, &env).map(|_| ()) } @@ -237,7 +237,7 @@ fn preflight_compile_invoke(invoke: &InvokeExpr, cgs: &CGS) -> Result<(), Runtim } })?; merge_entity_id_from_into_input_env(&mut env, target_ent, capability); - apply_preflight_compile_stubs(&mut env, capability); + apply_preflight_compile_stubs(&mut env, capability, cgs); merge_plasm_execute_session_env(&mut env); compile_operation_dispatch(&capability_template, &env).map(|_| ()) } diff --git a/crates/plasm-runtime/src/preflight.rs b/crates/plasm-runtime/src/preflight.rs index 5a48cc72..df825e63 100644 --- a/crates/plasm-runtime/src/preflight.rs +++ b/crates/plasm-runtime/src/preflight.rs @@ -121,7 +121,11 @@ pub(crate) async fn apply_preflight_steps( } /// Compile-only preflight: inject stub merge keys so CML templates compile without HTTP hydration. -pub(crate) fn apply_preflight_compile_stubs(env: &mut CmlEnv, capability: &CapabilitySchema) { +pub(crate) fn apply_preflight_compile_stubs( + env: &mut CmlEnv, + capability: &CapabilitySchema, + cgs: &CGS, +) { let Some(PreflightPlan(steps)) = capability.preflight.as_ref() else { return; }; @@ -133,7 +137,10 @@ pub(crate) fn apply_preflight_compile_stubs(env: &mut CmlEnv, capability: &Capab } for wire_key in merge.keys() { if env.get(wire_key).is_none() { - env.insert(wire_key.clone(), preflight_compile_stub_value(wire_key)); + env.insert( + wire_key.clone(), + preflight_wire_key_compile_stub_value(wire_key), + ); } } } @@ -145,7 +152,10 @@ pub(crate) fn apply_preflight_compile_stubs(env: &mut CmlEnv, capability: &Capab } for wire_key in merge.keys() { if env.get(wire_key).is_none() { - env.insert(wire_key.clone(), preflight_compile_stub_value(wire_key)); + env.insert( + wire_key.clone(), + preflight_wire_key_compile_stub_value(wire_key), + ); } } } @@ -161,13 +171,24 @@ pub(crate) fn apply_preflight_compile_stubs(env: &mut CmlEnv, capability: &Capab env.insert(merge.clone(), Value::Array(Vec::new())); } } - PreflightStep::HydrateInvokeTarget { .. } => {} + PreflightStep::HydrateInvokeTarget { get, prefix } => { + let Some(get_cap) = cgs.get_capability(get) else { + continue; + }; + let Some(entity) = cgs.get_entity(get_cap.domain.as_str()) else { + continue; + }; + for field_name in entity.fields.keys() { + let key = format!("{prefix}_{field_name}"); + env.insert(key.clone(), preflight_wire_key_compile_stub_value(&key)); + } + } PreflightStep::ExistenceCheck { .. } => {} } } } -fn preflight_compile_stub_value(wire_key: &str) -> Value { +fn preflight_wire_key_compile_stub_value(wire_key: &str) -> Value { if wire_key.ends_with("Id") || wire_key.ends_with("_id") || wire_key == "id" diff --git a/crates/plasm-runtime/tests/hydrate_invoke_target.rs b/crates/plasm-runtime/tests/hydrate_invoke_target.rs new file mode 100644 index 00000000..ce12bad0 --- /dev/null +++ b/crates/plasm-runtime/tests/hydrate_invoke_target.rs @@ -0,0 +1,45 @@ +use plasm_core::{Expr, InvokeExpr, Value, CGS}; +use plasm_runtime::{preflight_compile_expr, ViewAmbientContext}; + +fn fixture() -> CGS { + let dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../fixtures/schemas/hydrate_invoke_target"); + plasm_core::load_schema(&dir).expect("load hydrate_invoke_target fixture") +} + +fn run_expr(capability: &str) -> Expr { + let mut input = indexmap::indexmap! { + "expr".to_string() => Value::String("up".to_string()), + }; + if capability == "datasource_run" { + input.insert( + "from".to_string(), + Value::String("2026-08-03T03:50:00Z".to_string()), + ); + input.insert( + "to".to_string(), + Value::String("2026-08-03T04:00:00Z".to_string()), + ); + } + Expr::Invoke(InvokeExpr::new( + capability, + "Datasource", + "prometheus", + Some(Value::Object(input)), + )) +} + +#[test] +fn static_compile_hydrates_declared_entity_fields_and_honors_prefix() { + let cgs = fixture(); + let ambient = ViewAmbientContext::default(); + + preflight_compile_expr(&run_expr("datasource_run"), &cgs, &ambient) + .expect("ds_type comes from Datasource fields even though provides omits it"); + preflight_compile_expr(&run_expr("datasource_run_source_prefix"), &cgs, &ambient) + .expect("source_type should honor the configured prefix"); + + let error = preflight_compile_expr(&run_expr("datasource_run_typo"), &cgs, &ambient) + .expect_err("ds_typo must remain an unknown CML variable"); + assert!(error.to_string().contains("ds_typo"), "{error}"); +} diff --git a/fixtures/schemas/hydrate_invoke_target/domain.yaml b/fixtures/schemas/hydrate_invoke_target/domain.yaml new file mode 100644 index 00000000..c514f7fe --- /dev/null +++ b/fixtures/schemas/hydrate_invoke_target/domain.yaml @@ -0,0 +1,95 @@ +version: 1 +http_backend: http://localhost:1080 + +values: + nv_datasource_uid: + type: string + string_semantics: short + nv_datasource_type: + type: string + string_semantics: short + nv_query_expr: + type: string + string_semantics: short + nv_query_time: + type: string + string_semantics: short + nv_query_results: + type: json + +entities: + Datasource: + id_field: uid + description: Datasource used to test invoke-target preflight hydration. + fields: + uid: + required: true + value_ref: nv_datasource_uid + type: + required: true + value_ref: nv_datasource_type + query_results: + required: false + value_ref: nv_query_results + +capabilities: + datasource_get: + kind: get + entity: Datasource + provides: + - uid + # `type` is deliberately omitted: provides must not restrict decoded-row hydration. + + datasource_run: + kind: action + entity: Datasource + preflight: + - kind: hydrate_invoke_target + get: datasource_get + prefix: ds + parameters: + - name: expr + value_ref: nv_query_expr + required: true + - name: from + value_ref: nv_query_time + required: true + - name: to + value_ref: nv_query_time + required: true + provides: + - uid + - query_results + + datasource_run_typo: + kind: action + entity: Datasource + preflight: + - kind: hydrate_invoke_target + get: datasource_get + prefix: ds + parameters: + - name: expr + value_ref: nv_query_expr + required: true + provides: + - uid + - query_results + + datasource_run_source_prefix: + kind: action + entity: Datasource + preflight: + - kind: hydrate_invoke_target + get: datasource_get + prefix: source + parameters: + - name: expr + value_ref: nv_query_expr + required: true + provides: + - uid + - query_results + +auth: + scheme: none diff --git a/fixtures/schemas/hydrate_invoke_target/mappings.yaml b/fixtures/schemas/hydrate_invoke_target/mappings.yaml new file mode 100644 index 00000000..79778d1a --- /dev/null +++ b/fixtures/schemas/hydrate_invoke_target/mappings.yaml @@ -0,0 +1,60 @@ +datasource_get: + method: GET + path: + - { type: literal, value: api } + - { type: literal, value: datasources } + - { type: literal, value: uid } + - { type: var, name: id } + response: + single: true + +datasource_run: + method: POST + path: + - { type: literal, value: api } + - { type: literal, value: ds } + - { type: literal, value: query } + body: + type: object + fields: + - [queries, { type: array, elements: [ + { type: object, fields: [ + [datasource, { type: object, fields: [ + [type, { type: var, name: ds_type }], + [uid, { type: var, name: id }] + ]}], + [expr, { type: var, name: expr }] + ]} + ]}] + - [from, { type: var, name: from }] + - [to, { type: var, name: to }] + response: + single: true + +datasource_run_typo: + method: POST + path: + - { type: literal, value: api } + - { type: literal, value: ds } + - { type: literal, value: query } + body: + type: object + fields: + - [datasourceType, { type: var, name: ds_typo }] + - [expr, { type: var, name: expr }] + response: + single: true + +datasource_run_source_prefix: + method: POST + path: + - { type: literal, value: api } + - { type: literal, value: ds } + - { type: literal, value: query } + body: + type: object + fields: + - [datasourceType, { type: var, name: source_type }] + - [expr, { type: var, name: expr }] + response: + single: true