cli_engine/output/
pipeline.rs1use serde_json::Value;
2
3use crate::{CliCoreError, Result};
4
5use super::{PaginationMeta, filter_fields};
6
7#[derive(Clone, Debug, Default, Eq, PartialEq)]
9pub struct PipelineOpts {
10 pub filter: String,
12 pub limit: i64,
14 pub offset: i64,
16 pub expr: String,
18 pub fields: String,
20}
21
22pub fn apply_pipeline(data: &mut Value, opts: &PipelineOpts) -> Result<Option<PaginationMeta>> {
24 if !opts.filter.is_empty() {
25 apply_filter(data, &opts.filter)?;
26 }
27 let pagination = if opts.limit > 0 || opts.offset > 0 {
28 apply_pagination(data, opts.offset, opts.limit)?
29 } else {
30 None
31 };
32 if !opts.expr.is_empty() {
33 apply_expr(data, &opts.expr)?;
34 }
35 if !opts.fields.is_empty() {
36 *data = filter_fields(data, &opts.fields);
37 }
38 Ok(pagination)
39}
40
41fn apply_pagination(data: &mut Value, offset: i64, limit: i64) -> Result<Option<PaginationMeta>> {
42 let Value::Array(items) = data else {
43 return Ok(None);
44 };
45 let total = items.len();
46 let total_i64 = match i64::try_from(total) {
47 Ok(total) => total,
48 Err(_) => {
49 return Err(CliCoreError::message(
50 "pagination: list length exceeds supported range",
51 ));
52 }
53 };
54 let start = offset.min(total_i64);
55 let start = match usize::try_from(start) {
56 Ok(start) => start,
57 Err(_) => {
58 return Err(CliCoreError::message(
59 "pagination: offset must be non-negative",
60 ));
61 }
62 };
63 let mut end = total;
64 if limit > 0 {
65 let limit = match usize::try_from(limit) {
66 Ok(limit) => limit,
67 Err(_) => {
68 return Err(CliCoreError::message(
69 "pagination: limit exceeds supported range",
70 ));
71 }
72 };
73 if start + limit < end {
74 end = start + limit;
75 }
76 }
77 let sliced = items[start..end].to_vec();
78 *items = sliced;
79 Ok(Some(PaginationMeta {
80 total: total_i64,
81 offset,
82 limit,
83 count: match i64::try_from(end - start) {
84 Ok(count) => count,
85 Err(_) => {
86 return Err(CliCoreError::message(
87 "pagination: count exceeds supported range",
88 ));
89 }
90 },
91 }))
92}
93
94fn apply_filter(data: &mut Value, expression: &str) -> Result<()> {
95 let Value::Array(items) = data else {
96 return Err(CliCoreError::message(
97 "filter requires list data; use --expr for single objects",
98 ));
99 };
100
101 let expression = compile_query(expression)?;
102 let mut retained = Vec::with_capacity(items.len());
103 for item in items.drain(..) {
104 if search_query(&expression, &item)?.is_truthy() {
105 retained.push(item);
106 }
107 }
108 *items = retained;
109 Ok(())
110}
111
112fn apply_expr(data: &mut Value, expression: &str) -> Result<()> {
113 let expression = compile_query(expression)?;
114 let result = search_query(&expression, data)?;
115 *data = serde_json::to_value(result.as_ref())
116 .map_err(|error| CliCoreError::message(format!("expr: invalid result: {error}")))?;
117 Ok(())
118}
119
120fn compile_query(expression: &str) -> Result<jmespath::Expression<'static>> {
121 jmespath::compile(expression.trim())
122 .map_err(|error| CliCoreError::message(format!("expr: invalid JMESPath query: {error}")))
123}
124
125fn search_query(expression: &jmespath::Expression<'_>, data: &Value) -> Result<jmespath::Rcvar> {
126 expression
127 .search(data)
128 .map_err(|error| CliCoreError::message(format!("expr: JMESPath query failed: {error}")))
129}
130
131#[cfg(test)]
132mod tests {
133 use serde_json::json;
134
135 use super::{apply_expr, apply_pagination, compile_query, search_query};
136
137 #[test]
138 fn private_pipeline_helpers_cover_boundary_paths_directly() {
139 let mut object = json!({"id": "p1"});
140 assert_eq!(
141 apply_pagination(&mut object, 10, 1).expect("object pagination should no-op"),
142 None
143 );
144 assert_eq!(object, json!({"id": "p1"}));
145
146 let mut items = json!([{"id": "p1"}, {"id": "p2"}]);
147 let err =
148 apply_pagination(&mut items, -1, 1).expect_err("negative offset should be rejected");
149 assert_eq!(err.to_string(), "pagination: offset must be non-negative");
150
151 let expression = compile_query("items[?enabled].id").expect("query should compile");
152 let result = search_query(
153 &expression,
154 &json!({"items": [{"id": "p1", "enabled": true}, {"id": "p2", "enabled": false}]}),
155 )
156 .expect("query should evaluate");
157 assert_eq!(
158 serde_json::to_value(result.as_ref()).expect("result should serialize"),
159 json!(["p1"])
160 );
161
162 let mut data = json!({"items": [{"id": "p1"}]});
163 apply_expr(&mut data, "items[0].id").expect("expr should replace data");
164 assert_eq!(data, json!("p1"));
165 }
166}