Skip to main content

confium_daemon/methods/
attributes.rs

1//! Attribute-based threshold predicate methods.
2//!
3//! `attributes_evaluate` is the second stateless daemon handler
4//! (alongside `composite_verify`). It accepts a DSL expression +
5//! JSON list of signers, returns whether the predicate is satisfied.
6
7use std::collections::HashSet;
8
9use serde::Deserialize;
10use serde_json::{Map, Value, json};
11
12use crate::error::RpcError;
13use crate::server::SharedConfium;
14use confium_attributes::{SignerAttributes, evaluate, parse as dsl_parse};
15
16/// Request body for `attributes_evaluate`.
17#[derive(Debug, Deserialize)]
18struct AttributesEvaluateRequest {
19    /// DSL expression, e.g. `min_count("role:director", 3)`.
20    predicate: String,
21    /// JSON array of signer attribute maps. Each signer is an object
22    /// mapping attribute name → array of string values, e.g.
23    /// `[{"role:director": ["yes"], "region": ["europe"]}, ...]`.
24    signers: Vec<Map<String, Value>>,
25}
26
27/// Convert a JSON object (`{attr: [vals]}`) into a `SignerAttributes`.
28fn signer_from_json(obj: &Map<String, Value>) -> Result<SignerAttributes, RpcError> {
29    let mut attrs = SignerAttributes::new();
30    for (key, value) in obj {
31        let arr = value.as_array().ok_or_else(|| RpcError::InvalidParams {
32            detail: format!("attribute '{key}' value must be a JSON array of strings"),
33        })?;
34        let mut set = HashSet::new();
35        for v in arr {
36            let s = v.as_str().ok_or_else(|| RpcError::InvalidParams {
37                detail: format!("attribute '{key}' has a non-string value"),
38            })?;
39            set.insert(s.to_string());
40        }
41        attrs.attrs.insert(key.clone(), set);
42    }
43    Ok(attrs)
44}
45
46/// `attributes_evaluate({ "predicate": "...", "signers": [...] })`
47///
48/// Returns:
49/// ```json
50/// { "satisfied": true }
51/// ```
52pub async fn attributes_evaluate(
53    _cfm: SharedConfium,
54    params: Value,
55) -> std::result::Result<Value, RpcError> {
56    let req: AttributesEvaluateRequest =
57        serde_json::from_value(params).map_err(|e| RpcError::InvalidParams {
58            detail: format!("attributes_evaluate params: {e}"),
59        })?;
60
61    let predicate = dsl_parse(&req.predicate).map_err(|e| RpcError::InvalidParams {
62        detail: format!("predicate parse error: {e}"),
63    })?;
64
65    let mut signers = Vec::with_capacity(req.signers.len());
66    for obj in &req.signers {
67        signers.push(signer_from_json(obj)?);
68    }
69    let refs: Vec<&SignerAttributes> = signers.iter().collect();
70
71    let satisfied = evaluate(&predicate, &refs);
72    Ok(json!({ "satisfied": satisfied }))
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use crate::test_util::test_confium;
79
80    #[tokio::test]
81    async fn attributes_evaluate_satisfied() {
82        let params = json!({
83            "predicate": "min_count(\"role:director\", 3)",
84            "signers": [
85                {"role:director": ["yes"]},
86                {"role:director": ["yes"]},
87                {"role:director": ["yes"]},
88            ]
89        });
90        let result = attributes_evaluate(test_confium(), params).await.unwrap();
91        assert_eq!(result["satisfied"], json!(true));
92    }
93
94    #[tokio::test]
95    async fn attributes_evaluate_not_satisfied() {
96        let params = json!({
97            "predicate": "min_count(\"role:director\", 5)",
98            "signers": [
99                {"role:director": ["yes"]},
100                {"role:director": ["yes"]},
101            ]
102        });
103        let result = attributes_evaluate(test_confium(), params).await.unwrap();
104        assert_eq!(result["satisfied"], json!(false));
105    }
106
107    #[tokio::test]
108    async fn attributes_evaluate_distinct_values() {
109        let params = json!({
110            "predicate": "min_distinct(\"region\", 3)",
111            "signers": [
112                {"region": ["europe"]},
113                {"region": ["americas"]},
114                {"region": ["asia-pacific"]},
115            ]
116        });
117        let result = attributes_evaluate(test_confium(), params).await.unwrap();
118        assert_eq!(result["satisfied"], json!(true));
119    }
120
121    #[tokio::test]
122    async fn attributes_evaluate_rejects_bad_predicate() {
123        let params = json!({
124            "predicate": "bogus_function(\"arg\")",
125            "signers": [],
126        });
127        let result = attributes_evaluate(test_confium(), params).await;
128        assert!(result.is_err());
129    }
130
131    #[tokio::test]
132    async fn attributes_evaluate_rejects_missing_fields() {
133        let result = attributes_evaluate(test_confium(), json!({})).await;
134        assert!(result.is_err());
135    }
136
137    #[tokio::test]
138    async fn attributes_evaluate_rejects_non_array_signer_value() {
139        let params = json!({
140            "predicate": "any(\"x\")",
141            "signers": [{"x": "scalar"}],
142        });
143        let result = attributes_evaluate(test_confium(), params).await;
144        assert!(result.is_err());
145    }
146}