Skip to content
Merged
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
1 change: 1 addition & 0 deletions crates/plasm-cml/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod cml;
pub mod error;
pub(crate) mod gmail_send_body;
pub mod transport;
pub(crate) mod wire_normalize;

#[cfg(feature = "evm")]
pub mod evm_transport;
Expand Down
2 changes: 2 additions & 0 deletions crates/plasm-cml/src/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ pub enum CompiledOperation {
pub fn parse_capability_template(
template: &serde_json::Value,
) -> Result<CapabilityTemplate, CmlError> {
let template = &crate::wire_normalize::normalize_wire_cml_template(template.clone());
let transport = template
.get("transport")
.and_then(|v| v.as_str())
Expand Down Expand Up @@ -424,3 +425,4 @@ mod tests {
);
}
}

123 changes: 123 additions & 0 deletions crates/plasm-cml/src/wire_normalize.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//! Normalize authoring-wire mapping JSON into tagged CML before deserialization.
//!
//! Catalog `mappings.yaml` may attach optional body/query fields as:
//! ```yaml
//! - - status
//! - name: status
//! type: var
//! if:
//! exists: status
//! ```
//! That shorthand is not valid tagged [`crate::cml::CmlCond`] JSON (`type: exists`).
//! [`super::transport::parse_capability_template`] normalizes these nodes into
//! explicit `type: if` expressions before serde builds [`crate::cml::CmlExpr`].

use serde_json::{json, Value};

/// Walk a capability mapping template and expand optional `if: {exists: …}` field specs.
pub fn normalize_wire_cml_template(template: Value) -> Value {
normalize_wire_cml_value(template)
}

fn normalize_wire_cml_value(value: Value) -> Value {
match value {
Value::Object(mut map) => {
if map.contains_key("type") && map.contains_key("if") {
let cond_raw = map
.remove("if")
.expect("if key present when contains_key true");
let inner = normalize_wire_cml_value(Value::Object(map));
return wrap_cml_if(normalize_cml_cond_value(cond_raw), inner);
}

if map.get("type").and_then(Value::as_str) == Some("object") {
if let Some(Value::Array(fields)) = map.get_mut("fields") {
for entry in fields.iter_mut() {
if let Value::Array(pair) = entry {
if pair.len() == 2 {
pair[1] = normalize_wire_cml_value(pair[1].take());
}
}
}
}
}

let mut out = serde_json::Map::new();
for (k, v) in map {
out.insert(k, normalize_wire_cml_value(v));
}
Value::Object(out)
}
Value::Array(items) => Value::Array(
items
.into_iter()
.map(normalize_wire_cml_value)
.collect(),
),
other => other,
}
}

fn wrap_cml_if(condition: Value, then_expr: Value) -> Value {
json!({
"type": "if",
"condition": condition,
"then_expr": then_expr,
"else_expr": { "type": "const", "value": null }
})
}

fn normalize_cml_cond_value(value: Value) -> Value {
if let Value::Object(map) = &value {
if map.len() == 1 {
if let Some(Value::String(var)) = map.get("exists") {
return json!({ "type": "exists", "var": var });
}
}
}
normalize_wire_cml_value(value)
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{compile_operation, parse_capability_template, CmlEnv};
use plasm_core::Value as PlasmValue;

#[test]
fn optional_var_field_if_exists_shorthand_compiles() {
let v = json!({
"method": "POST",
"path": [{"type": "literal", "value": "v1"}],
"body": {
"type": "object",
"fields": [
["credit_card_account_id", {"type": "var", "name": "credit_card_account_id"}],
["status", {"type": "var", "name": "status", "if": {"exists": "status"}}]
]
}
});
let t = parse_capability_template(&v).unwrap();
let mut env = CmlEnv::new();
env.insert(
"credit_card_account_id".into(),
PlasmValue::String("cc".into()),
);
let compiled = compile_operation(&t, &env).expect("omit optional status");
let crate::CompiledOperation::Http(req) = compiled else {
panic!("expected http");
};
let obj = req.body.as_ref().unwrap().as_object().unwrap();
assert!(!obj.contains_key("status"));
env.insert("status".into(), PlasmValue::String("PENDING".into()));
let compiled = compile_operation(&t, &env).expect("include status when bound");
let crate::CompiledOperation::Http(req) = compiled else {
panic!("expected http");
};
let obj = req.body.as_ref().unwrap().as_object().unwrap();
assert_eq!(
obj.get("status"),
Some(&PlasmValue::String("PENDING".into()))
);
}
}
1 change: 1 addition & 0 deletions crates/plasm-runtime/src/view_matrix_fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub const MATRIX_VIEW_PREFLIGHT_CASES: &[(&str, &str)] = &[
("lang_triage_context", "LangTriageContext"),
("lang_item_link", "LangItemLink"),
("lang_owner_filter_demo", "LangOwnerFilterDemo"),
("lang_tag_filter_demo", "LangTagFilterDemo"),
("lang_work_snapshot", "LangWorkSnapshot"),
("lang_work_snapshot_empty", "LangWorkSnapshotEmpty"),
];
Expand Down
34 changes: 27 additions & 7 deletions crates/plasm-runtime/src/view_preflight.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,14 @@ impl ViewNodeRunner for PreflightViewNodeRunner<'_> {
node_fields: &ViewNodeFieldMap,
) -> Result<ExecutionResult, RuntimeError> {
let q = QueryExpr::filtered(cap.domain.as_str(), pred.clone());
preflight_compile_expr(&plasm_core::Expr::Query(q), self.cgs, self.ambient)?;
preflight_compile_expr(&plasm_core::Expr::Query(q), self.cgs, self.ambient).map_err(
|e| RuntimeError::ConfigurationError {
message: format!(
"view `{}` node `{}` (capability `{}`): {e}",
ctx.view_name, node.id, node.capability
),
},
)?;
let mut bound_values = IndexMap::with_capacity(node.bind.len());
for (param, bspec) in &node.bind {
bound_values.insert(
Expand All @@ -86,28 +93,41 @@ impl ViewNodeRunner for PreflightViewNodeRunner<'_> {

fn run_get_node(
&self,
_ctx: &ViewRunContext<'_>,
_node: &plasm_core::schema::ViewNodeSpec,
ctx: &ViewRunContext<'_>,
node: &plasm_core::schema::ViewNodeSpec,
cap: &plasm_core::CapabilitySchema,
get: &GetExpr,
bound: &BTreeMap<String, String>,
) -> Result<ExecutionResult, RuntimeError> {
preflight_compile_expr(&plasm_core::Expr::Get(get.clone()), self.cgs, self.ambient)?;
preflight_compile_expr(&plasm_core::Expr::Get(get.clone()), self.cgs, self.ambient).map_err(
|e| RuntimeError::ConfigurationError {
message: format!(
"view `{}` node `{}` (capability `{}`): {e}",
ctx.view_name, node.id, node.capability
),
},
)?;
stub_get_result(cap, self.cgs, bound)
}

fn run_create_node(
&self,
_ctx: &ViewRunContext<'_>,
_node: &plasm_core::schema::ViewNodeSpec,
ctx: &ViewRunContext<'_>,
node: &plasm_core::schema::ViewNodeSpec,
cap: &plasm_core::CapabilitySchema,
create: &plasm_core::CreateExpr,
) -> Result<ExecutionResult, RuntimeError> {
preflight_compile_expr(
&plasm_core::Expr::Create(create.clone()),
self.cgs,
self.ambient,
)?;
)
.map_err(|e| RuntimeError::ConfigurationError {
message: format!(
"view `{}` node `{}` (capability `{}`): {e}",
ctx.view_name, node.id, node.capability
),
})?;
stub_query_result(cap, self.cgs, &IndexMap::new())
}
}
21 changes: 21 additions & 0 deletions crates/plasm-runtime/tests/view_literal_optional_cml_preflight.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//! Regression: view DAG nodes with literal param binds must compile optional CML
//! `if: {exists: …}` body/query fields (see `plasm-cml::wire_normalize`).

use plasm_runtime::{
preflight_view_query,
view_test_support::{matrix_view_query, matrix_views_cgs},
ViewAmbientContext,
};

#[test]
fn matrix_lang_tag_filter_demo_literal_label_bind_preflight() {
let cgs = matrix_views_cgs();
let query = matrix_view_query("LangTagFilterDemo");
preflight_view_query(
"lang_tag_filter_demo",
&query,
&cgs,
&ViewAmbientContext::default(),
)
.unwrap_or_else(|e| panic!("lang_tag_filter_demo preflight: {e}"));
}
64 changes: 64 additions & 0 deletions fixtures/schemas/plasm_language_matrix_views/domain.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,18 @@ entities:
tags:
target: LangTag
cardinality: many
LangTagFilterDemo:
id_field: item_id
primary_read: lang_tag_filter_demo_get
description: View-backed tag list with literal label filter (optional CML if-exists conformance).
fields:
item_id:
required: true
value_ref: nv_lang_digest_item_id
tag_count:
required: false
value_ref: nv_wire_int
relations: {}
LangItemLink:
id_field: item_id
primary_read: lang_item_link_get
Expand Down Expand Up @@ -313,6 +325,10 @@ capabilities:
value_ref: nv_langtag_query_item_id
required: true
role: scope
- name: label
value_ref: nv_lang_tag_label
required: false
role: filter
provides:
- id
- item_id
Expand Down Expand Up @@ -444,6 +460,30 @@ capabilities:
- item_id
- owner
- matching_item_count
lang_tag_filter_demo_query:
kind: query
entity: LangTagFilterDemo
description: Query tag filter demo by item id (literal label bind in view DAG).
parameters:
- name: item_id
value_ref: nv_lang_digest_scope_item_id
required: true
role: filter
provides:
- item_id
- tag_count
lang_tag_filter_demo_get:
kind: get
entity: LangTagFilterDemo
description: Get tag filter demo by item id.
parameters:
- name: item_id
value_ref: nv_lang_digest_get_item_id
required: true
role: scope
provides:
- item_id
- tag_count
langviewer_get:
kind: get
entity: LangViewer
Expand Down Expand Up @@ -724,6 +764,30 @@ views:
binding:
kind: node_single_row
node: item_node
lang_tag_filter_demo:
description: Count LangTags for one item with a fixed label filter (literal view bind).
capability: lang_tag_filter_demo_query
entity: LangTagFilterDemo
scope:
- name: item_id
value_ref: nv_lang_digest_scope_item_id
nodes:
- id: tagged_rows
capability: langtag_query
bind:
item_id:
kind: scope
param: item_id
label:
kind: literal
value: urgent
output:
item_id:
kind: scope
param: item_id
tag_count:
kind: node_row_count
node: tagged_rows
lang_work_snapshot:
description: Parameterless dashboard snapshot with assigned LangItems (MyWorkSnapshot pattern).
capability: lang_work_snapshot_query
Expand Down
13 changes: 13 additions & 0 deletions fixtures/schemas/plasm_language_matrix_views/mappings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ langtag_query:
type: object
fields:
- ["item_id", { type: var, name: item_id }]
- - label
- name: label
type: var
if:
exists: label

langline_query:
method: GET
Expand Down Expand Up @@ -182,6 +187,14 @@ lang_owner_filter_demo_get:
transport: view
view: lang_owner_filter_demo

lang_tag_filter_demo_query:
transport: view
view: lang_tag_filter_demo

lang_tag_filter_demo_get:
transport: view
view: lang_tag_filter_demo

langviewer_get:
method: GET
path:
Expand Down
Loading