Skip to main content

postgrest_parser/parser/
rpc.rs

1use crate::ast::RpcParams;
2use crate::error::{Error, ParseError};
3use crate::parser::{parse_json_body, parse_order, parse_select};
4use serde_json::Value;
5use std::collections::HashMap;
6
7/// Parses RPC parameters from query string and body
8///
9/// # Arguments
10///
11/// * `function_name` - Name of the function to call
12/// * `query_string` - Query parameters (e.g., "limit=10&order=created_at.desc")
13/// * `body` - Optional JSON body with function arguments
14///
15/// # Examples
16///
17/// ```
18/// use postgrest_parser::parser::parse_rpc_params;
19///
20/// let body = r#"{"user_id": 123, "status": "active"}"#;
21/// let params = parse_rpc_params("get_user_posts", "", Some(body)).unwrap();
22/// assert_eq!(params.function_name, "get_user_posts");
23/// ```
24pub fn parse_rpc_params(
25    function_name: &str,
26    query_string: &str,
27    body: Option<&str>,
28) -> Result<RpcParams, Error> {
29    // Parse arguments from body if present
30    let args = if let Some(body_str) = body {
31        let json_value = parse_json_body(body_str)?;
32        validate_rpc_args(json_value)?
33    } else {
34        HashMap::new()
35    };
36
37    let mut params = RpcParams::new(function_name, args);
38
39    // Parse query parameters
40    let query_params = parse_query_params(query_string);
41
42    // Parse filters - all non-reserved keys become filters
43    let filter_pairs: Vec<(String, String)> = query_params
44        .iter()
45        .filter(|(k, _)| !is_reserved_key(k))
46        .map(|(k, v)| (k.clone(), v.clone()))
47        .collect();
48
49    if !filter_pairs.is_empty() {
50        let parsed_params = crate::parse_params_from_pairs(filter_pairs)?;
51        params = params.with_filters(parsed_params.filters);
52    }
53
54    // Parse order clause
55    if let Some(order_str) = query_params.get("order") {
56        let order = parse_order(order_str)?;
57        params = params.with_order(order);
58    }
59
60    // Parse limit
61    if let Some(limit_str) = query_params.get("limit") {
62        let limit = limit_str
63            .parse::<u64>()
64            .map_err(|_| Error::Parse(ParseError::InvalidLimit(limit_str.to_string())))?;
65        params = params.with_limit(limit);
66    }
67
68    // Parse offset
69    if let Some(offset_str) = query_params.get("offset") {
70        let offset = offset_str
71            .parse::<u64>()
72            .map_err(|_| Error::Parse(ParseError::InvalidOffset(offset_str.to_string())))?;
73        params = params.with_offset(offset);
74    }
75
76    // Parse returning (select) clause
77    if let Some(select_str) = query_params.get("select") {
78        let returning = parse_select(select_str)?;
79        params = params.with_returning(returning);
80    }
81
82    Ok(params)
83}
84
85fn validate_rpc_args(value: Value) -> Result<HashMap<String, Value>, Error> {
86    match value {
87        Value::Object(map) => {
88            let mut hash_map = HashMap::new();
89            for (k, v) in map {
90                hash_map.insert(k, v);
91            }
92            Ok(hash_map)
93        }
94        _ => Err(Error::Parse(ParseError::InvalidJsonBody(
95            "RPC arguments must be a JSON object".to_string(),
96        ))),
97    }
98}
99
100fn parse_query_params(query_string: &str) -> HashMap<String, String> {
101    if query_string.is_empty() {
102        return HashMap::new();
103    }
104
105    query_string
106        .split('&')
107        .filter_map(|pair| {
108            let parts: Vec<&str> = pair.splitn(2, '=').collect();
109            if parts.len() == 2 {
110                Some((parts[0].to_string(), parts[1].to_string()))
111            } else {
112                None
113            }
114        })
115        .collect()
116}
117
118fn is_reserved_key(key: &str) -> bool {
119    matches!(key, "select" | "order" | "limit" | "offset")
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn test_parse_rpc_params_with_body() {
128        let body = r#"{"user_id": 123, "status": "active"}"#;
129        let params = parse_rpc_params("get_user_posts", "", Some(body)).unwrap();
130
131        assert_eq!(params.function_name, "get_user_posts");
132        assert_eq!(params.args.len(), 2);
133        assert_eq!(params.args.get("user_id"), Some(&Value::Number(123.into())));
134    }
135
136    #[test]
137    fn test_parse_rpc_params_no_body() {
138        let params = parse_rpc_params("health_check", "", None).unwrap();
139
140        assert_eq!(params.function_name, "health_check");
141        assert!(params.args.is_empty());
142    }
143
144    #[test]
145    fn test_parse_rpc_params_with_limit_offset() {
146        let params = parse_rpc_params("list_users", "limit=10&offset=20", None).unwrap();
147
148        assert_eq!(params.limit, Some(10));
149        assert_eq!(params.offset, Some(20));
150    }
151
152    #[test]
153    fn test_parse_rpc_params_with_order() {
154        let params = parse_rpc_params("list_users", "order=created_at.desc", None).unwrap();
155
156        assert_eq!(params.order.len(), 1);
157    }
158
159    #[test]
160    fn test_parse_rpc_params_with_filters() {
161        let params = parse_rpc_params("list_users", "age=gt.18&status=eq.active", None).unwrap();
162
163        assert_eq!(params.filters.len(), 2);
164    }
165
166    #[test]
167    fn test_parse_rpc_params_with_select() {
168        let params = parse_rpc_params("get_posts", "select=id,title,author", None).unwrap();
169
170        assert!(params.returning.is_some());
171        assert_eq!(params.returning.unwrap().len(), 3);
172    }
173
174    #[test]
175    fn test_parse_rpc_invalid_body() {
176        let body = r#"["not", "an", "object"]"#;
177        let result = parse_rpc_params("test_func", "", Some(body));
178
179        assert!(result.is_err());
180    }
181
182    #[test]
183    fn test_parse_rpc_complex_scenario() {
184        let body = r#"{"department_id": 5}"#;
185        let query = "age=gte.25&salary=lt.100000&order=name.asc&limit=50&select=id,name,salary";
186        let params = parse_rpc_params("find_employees", query, Some(body)).unwrap();
187
188        assert_eq!(params.function_name, "find_employees");
189        assert_eq!(params.args.len(), 1);
190        assert_eq!(params.filters.len(), 2);
191        assert_eq!(params.order.len(), 1);
192        assert_eq!(params.limit, Some(50));
193        assert!(params.returning.is_some());
194    }
195}