confium_daemon/methods/
attributes.rs1use 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#[derive(Debug, Deserialize)]
18struct AttributesEvaluateRequest {
19 predicate: String,
21 signers: Vec<Map<String, Value>>,
25}
26
27fn 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
46pub 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}