this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feature: support per-matcher AT-URI

Signed-off-by: Nick Gerakines <12125+ngerakines@users.noreply.github.com>

+271 -132
+1 -1
Cargo.toml
··· 8 8 strip = true 9 9 10 10 [features] 11 - default = [] 11 + default = [""] 12 12 rhai = ["dep:rhai"] 13 13 14 14 [dependencies]
+2 -2
docs/playbook-match-likes.md
··· 8 8 9 9 To match on likes, we need to make 2 changes: 10 10 11 - 1. Add the `aturi` attribute to the feed configuration. 11 + 1. Add the `aturi` attribute to the feed configuration for the matcher. 12 12 2. Set the environment value `COLLECTIONS` to include `app.bsky.feed.like,app.bsky.feed.post`. When not explicitly set, the default value is `app.bsky.feed.post`. 13 13 14 14 ## Example ··· 22 22 - uri: "at://did:plc:decafbad/app.bsky.feed.generator/my_popular_posts" 23 23 name: "My popular posts" 24 24 description: "Posts that I've made that have been liked." 25 - aturi: "$.commit.record.subject.uri" 26 25 matchers: 27 26 - path: "$.commit.record.subject.uri" 28 27 value: "at://did:plc:decafbad/app.bsky.feed.post/" 29 28 type: prefix 29 + aturi: "$.commit.record.subject.uri" 30 30 ``` 31 31
+11 -1
docs/playbook-rhai.md
··· 32 32 result 33 33 ``` 34 34 35 + ## Provided Methods 36 + 37 + The following methods are available to rhai scripts: 38 + 39 + * `build_aturi(event: Event) -> String` - Build an AT-URI from an event. For feed posts, this composes an AT-URI from the event DID, commit collection, and commit rkey. 40 + 35 41 ## Usage 36 42 37 43 The feed matcher type `rhai` is used with a `source` attribute that points to an rhai script file. ··· 65 71 66 72 let root_uri = event?.commit?.record?.reply?.root?.uri; 67 73 68 - result.matched = root_uri.starts_with("at://did:plc:cbkjy5n7bk3ax2wplmtjofq2/app.bsky.feed.post/"); 74 + result.matched = `${root_uri}`.starts_with("at://did:plc:cbkjy5n7bk3ax2wplmtjofq2/app.bsky.feed.post/"); 75 + 76 + if result.matched { 77 + result.aturi = build_aturi(event); 78 + } 69 79 70 80 result 71 81 ```
+15 -3
src/config.rs
··· 30 30 #[serde(tag = "type")] 31 31 pub enum Matcher { 32 32 #[serde(rename = "equal")] 33 - Equal { path: String, value: String }, 33 + Equal { 34 + path: String, 35 + value: String, 36 + aturi: Option<String>, 37 + }, 34 38 35 39 #[serde(rename = "prefix")] 36 - Prefix { path: String, value: String }, 40 + Prefix { 41 + path: String, 42 + value: String, 43 + aturi: Option<String>, 44 + }, 37 45 38 46 #[serde(rename = "sequence")] 39 - Sequence { path: String, values: Vec<String> }, 47 + Sequence { 48 + path: String, 49 + values: Vec<String>, 50 + aturi: Option<String>, 51 + }, 40 52 41 53 #[serde(rename = "rhai")] 42 54 Rhai { script: String },
+10 -40
src/consumer.rs
··· 142 142 } 143 143 let decoded = decoded.unwrap(); 144 144 serde_json::from_slice::<model::Event>(&decoded) 145 + .context(anyhow!("cannot deserialize message")) 145 146 } else { 146 147 if !item.is_text() { 147 148 tracing::debug!("compression enabled but message from jetstream is not binary"); 148 149 continue; 149 150 } 150 - serde_json::from_str(item.as_text().ok_or(anyhow!("cannot convert message to text"))?) 151 + item.as_text() 152 + .ok_or(anyhow!("cannot convert message to text")) 153 + .and_then(|value| { 154 + serde_json::from_str::<model::Event>(value) 155 + .context(anyhow!("cannot deserialize message")) 156 + }) 151 157 }; 152 158 if let Err(err) = event { 153 159 tracing::error!(error = ?err, "error processing jetstream message"); ··· 170 176 let event_value = event_value.unwrap(); 171 177 172 178 for feed_matcher in self.feed_matchers.0.iter() { 173 - if feed_matcher.matches(&event_value) { 179 + if let Some(match_result) = feed_matcher.matches(&event_value) { 174 180 tracing::debug!(feed_id = ?feed_matcher.feed, "matched event"); 175 - if let Some(uri) = model::to_post_strong_ref(feed_matcher.aturi.as_ref(), &event, &event_value) { 181 + if match_result.matched { 176 182 let feed_content = storage::model::FeedContent{ 177 183 feed_id: feed_matcher.feed.clone(), 178 - uri, 184 + uri: match_result.aturi, 179 185 indexed_at: event.clone().time_us, 180 186 }; 181 187 feed_content_insert(&self.pool, &feed_content).await?; ··· 197 203 use std::collections::HashMap; 198 204 199 205 use serde::{Deserialize, Serialize}; 200 - use serde_json_path::JsonPath; 201 206 202 207 #[derive(Debug, Clone, Serialize, Deserialize)] 203 208 #[serde(tag = "type", content = "payload")] ··· 288 293 pub(crate) kind: String, 289 294 pub(crate) time_us: i64, 290 295 pub(crate) commit: Option<CommitOp>, 291 - } 292 - 293 - pub(crate) fn to_post_strong_ref( 294 - aturi: Option<&JsonPath>, 295 - event: &Event, 296 - event_value: &serde_json::Value, 297 - ) -> Option<String> { 298 - if let Some(CommitOp::Create { 299 - collection, rkey, .. 300 - }) = &event.commit 301 - { 302 - if let Some(aturi_path) = aturi { 303 - let nodes = aturi_path.query(event_value).all(); 304 - let string_nodes = nodes 305 - .iter() 306 - .filter_map(|value| { 307 - if let serde_json::Value::String(actual) = value { 308 - Some(actual.to_lowercase().clone()) 309 - } else { 310 - None 311 - } 312 - }) 313 - .collect::<Vec<String>>(); 314 - 315 - for value in string_nodes { 316 - if value.starts_with("at://") { 317 - return Some(value); 318 - } 319 - } 320 - } 321 - 322 - let uri = format!("at://{}/{}/{}", event.did, collection, rkey); 323 - return Some(uri); 324 - } 325 - None 326 296 } 327 297 }
+214 -82
src/matcher.rs
··· 1 - use anyhow::{Context, Result}; 2 - 3 - #[cfg(not(feature = "rhai"))] 4 - use anyhow::anyhow; 1 + use anyhow::{anyhow, Context, Result}; 5 2 6 3 use serde_json_path::JsonPath; 7 4 8 5 use crate::config; 9 6 10 - pub trait Matcher: Sync + Send { 11 - fn matches(&self, value: &serde_json::Value) -> bool; 12 - } 13 - 14 7 #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] 15 8 pub struct MatcherResult { 16 9 pub matched: bool, 17 10 pub aturi: String, 18 - pub score: i64, 11 + } 12 + 13 + impl PartialEq<bool> for MatcherResult { 14 + fn eq(&self, other: &bool) -> bool { 15 + self.matched == *other 16 + } 19 17 } 20 18 21 19 impl MatcherResult { ··· 34 32 fn set_aturi(&mut self, value: String) { 35 33 self.aturi = value; 36 34 } 35 + } 37 36 38 - fn get_score(&mut self) -> i64 { 39 - self.score 40 - } 41 - 42 - fn set_score(&mut self, value: i64) { 43 - self.score = value; 44 - } 37 + pub trait Matcher: Sync + Send { 38 + fn matches(&self, value: &serde_json::Value) -> Result<MatcherResult>; 45 39 } 46 40 47 41 pub struct FeedMatcher { 48 42 pub(crate) feed: String, 49 - pub(crate) aturi: Option<serde_json_path::JsonPath>, 50 43 matchers: Vec<Box<dyn Matcher>>, 51 44 } 52 45 ··· 59 52 for config_feed in config_feeds.feeds.iter() { 60 53 let feed = config_feed.uri.clone(); 61 54 62 - let aturi = config_feed 63 - .aturi 64 - .as_ref() 65 - .and_then(|value| JsonPath::parse(value).ok()); 66 - 67 55 let mut matchers = vec![]; 68 56 69 57 for config_feed_matcher in config_feed.matchers.iter() { 70 58 match config_feed_matcher { 71 - config::Matcher::Equal { path, value } => { 59 + config::Matcher::Equal { path, value, aturi } => { 72 60 matchers 73 - .push(Box::new(EqualsMatcher::new(value, path)?) as Box<dyn Matcher>); 61 + .push(Box::new(EqualsMatcher::new(value, path, aturi)?) 62 + as Box<dyn Matcher>); 74 63 } 75 - config::Matcher::Prefix { path, value } => { 64 + config::Matcher::Prefix { path, value, aturi } => { 76 65 matchers 77 - .push(Box::new(PrefixMatcher::new(value, path)?) as Box<dyn Matcher>); 66 + .push(Box::new(PrefixMatcher::new(value, path, aturi)?) 67 + as Box<dyn Matcher>); 78 68 } 79 - config::Matcher::Sequence { path, values } => { 80 - matchers.push(Box::new(SequenceMatcher::new(values, path)?) as Box<dyn Matcher>); 69 + config::Matcher::Sequence { 70 + path, 71 + values, 72 + aturi, 73 + } => { 74 + matchers.push(Box::new(SequenceMatcher::new(values, path, aturi)?) 75 + as Box<dyn Matcher>); 81 76 } 82 77 83 78 #[cfg(feature = "rhai")] ··· 93 88 } 94 89 } 95 90 96 - feed_matchers.push(FeedMatcher { 97 - feed, 98 - aturi, 99 - matchers, 100 - }); 91 + feed_matchers.push(FeedMatcher { feed, matchers }); 101 92 } 102 93 103 94 Ok(Self(feed_matchers)) ··· 105 96 } 106 97 107 98 impl FeedMatcher { 108 - pub(crate) fn matches(&self, value: &serde_json::Value) -> bool { 109 - self.matchers.iter().any(|matcher| matcher.matches(value)) 99 + pub(crate) fn matches(&self, value: &serde_json::Value) -> Option<MatcherResult> { 100 + for matcher in self.matchers.iter() { 101 + let result = matcher.matches(value); 102 + if let Err(err) = result { 103 + tracing::error!(error = ?err, "matcher returned error"); 104 + continue; 105 + } 106 + let result = result.unwrap(); 107 + if result.matched { 108 + return Some(result); 109 + } 110 + } 111 + None 110 112 } 111 113 } 112 114 113 115 pub struct EqualsMatcher { 114 116 expected: String, 115 117 path: JsonPath, 118 + aturi_path: Option<JsonPath>, 116 119 } 117 120 118 121 impl EqualsMatcher { 119 - pub fn new(expected: &str, path: &str) -> Result<Self> { 122 + pub fn new(expected: &str, path: &str, aturi: &Option<String>) -> Result<Self> { 120 123 let path = JsonPath::parse(path).context("cannot parse path")?; 124 + let aturi_path = if let Some(aturi) = aturi { 125 + let parsed_aturi_path = 126 + JsonPath::parse(aturi).context("cannot parse aturi jsonpath")?; 127 + Some(parsed_aturi_path) 128 + } else { 129 + None 130 + }; 121 131 Ok(Self { 122 132 expected: expected.to_string(), 123 133 path, 134 + aturi_path, 124 135 }) 125 136 } 126 137 } 127 138 128 139 impl Matcher for EqualsMatcher { 129 - fn matches(&self, value: &serde_json::Value) -> bool { 140 + fn matches(&self, value: &serde_json::Value) -> Result<MatcherResult> { 130 141 let nodes = self.path.query(value).all(); 131 142 132 143 let string_nodes = nodes ··· 140 151 }) 141 152 .collect::<Vec<String>>(); 142 153 143 - string_nodes.iter().any(|value| value == &self.expected) 154 + if string_nodes.iter().any(|value| value == &self.expected) { 155 + let aturi = extract_aturi(self.aturi_path.as_ref(), value) 156 + .ok_or(anyhow!("matcher matched but could not create at-uri"))?; 157 + Ok(MatcherResult { 158 + matched: true, 159 + aturi, 160 + }) 161 + } else { 162 + Ok(MatcherResult::default()) 163 + } 144 164 } 145 165 } 146 166 147 167 pub struct PrefixMatcher { 148 168 prefix: String, 149 169 path: JsonPath, 170 + aturi_path: Option<JsonPath>, 150 171 } 151 172 152 173 impl PrefixMatcher { 153 - pub(crate) fn new(prefix: &str, path: &str) -> Result<Self> { 174 + pub(crate) fn new(prefix: &str, path: &str, aturi: &Option<String>) -> Result<Self> { 154 175 let path = JsonPath::parse(path).context("cannot parse path")?; 176 + let aturi_path = if let Some(aturi) = aturi { 177 + let parsed_aturi_path = 178 + JsonPath::parse(aturi).context("cannot parse aturi jsonpath")?; 179 + Some(parsed_aturi_path) 180 + } else { 181 + None 182 + }; 155 183 Ok(Self { 156 184 prefix: prefix.to_string(), 157 185 path, 186 + aturi_path, 158 187 }) 159 188 } 160 189 } 161 190 162 191 impl Matcher for PrefixMatcher { 163 - fn matches(&self, value: &serde_json::Value) -> bool { 192 + fn matches(&self, value: &serde_json::Value) -> Result<MatcherResult> { 164 193 let nodes = self.path.query(value).all(); 165 194 166 195 let string_nodes = nodes ··· 174 203 }) 175 204 .collect::<Vec<String>>(); 176 205 177 - string_nodes 206 + let found = string_nodes 178 207 .iter() 179 - .any(|value| value.starts_with(&self.prefix)) 208 + .any(|value| value.starts_with(&self.prefix)); 209 + if found { 210 + let aturi = extract_aturi(self.aturi_path.as_ref(), value) 211 + .ok_or(anyhow!("matcher matched but could not create at-uri"))?; 212 + Ok(MatcherResult { 213 + matched: true, 214 + aturi, 215 + }) 216 + } else { 217 + Ok(MatcherResult::default()) 218 + } 180 219 } 181 220 } 182 221 183 222 pub struct SequenceMatcher { 184 223 expected: Vec<String>, 185 224 path: JsonPath, 225 + aturi_path: Option<JsonPath>, 186 226 } 187 227 188 228 impl SequenceMatcher { 189 - pub(crate) fn new(expected: &[String], path: &str) -> Result<Self> { 229 + pub(crate) fn new(expected: &[String], path: &str, aturi: &Option<String>) -> Result<Self> { 190 230 let path = JsonPath::parse(path).context("cannot parse path")?; 231 + let aturi_path = if let Some(aturi) = aturi { 232 + let parsed_aturi_path = 233 + JsonPath::parse(aturi).context("cannot parse aturi jsonpath")?; 234 + Some(parsed_aturi_path) 235 + } else { 236 + None 237 + }; 191 238 Ok(Self { 192 239 expected: expected.to_owned(), 193 240 path, 241 + aturi_path, 194 242 }) 195 243 } 196 244 } 197 245 198 246 impl Matcher for SequenceMatcher { 199 - fn matches(&self, value: &serde_json::Value) -> bool { 247 + fn matches(&self, value: &serde_json::Value) -> Result<MatcherResult> { 200 248 let nodes = self.path.query(value).all(); 201 249 202 250 let string_nodes = nodes ··· 230 278 } 231 279 232 280 if last_found != -1 && found_index == self.expected.len() - 1 { 233 - return true; 281 + let aturi = extract_aturi(self.aturi_path.as_ref(), value) 282 + .ok_or(anyhow!("matcher matched but could not create at-uri"))?; 283 + return Ok(MatcherResult { 284 + matched: true, 285 + aturi, 286 + }); 234 287 } 235 288 } 236 289 237 - false 290 + Ok(MatcherResult::default()) 238 291 } 239 292 } 240 293 294 + fn extract_aturi(aturi: Option<&JsonPath>, event_value: &serde_json::Value) -> Option<String> { 295 + if let Some(aturi_path) = aturi { 296 + let nodes = aturi_path.query(event_value).all(); 297 + let string_nodes = nodes 298 + .iter() 299 + .filter_map(|value| { 300 + if let serde_json::Value::String(actual) = value { 301 + Some(actual.to_lowercase().clone()) 302 + } else { 303 + None 304 + } 305 + }) 306 + .collect::<Vec<String>>(); 307 + 308 + for value in string_nodes { 309 + if value.starts_with("at://") { 310 + return Some(value); 311 + } 312 + } 313 + } 314 + 315 + let rtype = event_value 316 + .get("commit") 317 + .and_then(|commit| commit.get("record")) 318 + .and_then(|commit| commit.get("$type")) 319 + .and_then(|did| did.as_str()); 320 + 321 + if Some("app.bsky.feed.post") == rtype { 322 + let did = event_value.get("did").and_then(|did| did.as_str())?; 323 + let commit = event_value.get("commit")?; 324 + let collection = commit.get("collection").and_then(|did| did.as_str())?; 325 + let rkey = commit.get("rkey").and_then(|did| did.as_str())?; 326 + let uri = format!("at://{}/{}/{}", did, collection, rkey); 327 + return Some(uri); 328 + } 329 + 330 + None 331 + } 332 + 241 333 #[cfg(feature = "rhai")] 242 334 pub mod rhai { 243 335 244 336 use super::{Matcher, MatcherResult}; 245 - use anyhow::{Context, Result}; 337 + use anyhow::{anyhow, Context, Result}; 246 338 247 - use rhai::{serde::to_dynamic, Engine, Scope, AST}; 339 + use rhai::{serde::to_dynamic, Dynamic, Engine, Scope, AST}; 248 340 use std::{path::PathBuf, str::FromStr}; 249 341 250 342 pub struct RhaiMatcher { ··· 263 355 MatcherResult::get_matched, 264 356 MatcherResult::set_matched, 265 357 ) 266 - .register_get_set("score", MatcherResult::get_score, MatcherResult::set_score) 267 358 .register_get_set("aturi", MatcherResult::get_aturi, MatcherResult::set_aturi) 268 - .register_fn("new_matcher_result", MatcherResult::default); 359 + .register_fn("new_matcher_result", MatcherResult::default) 360 + .register_fn("build_aturi", build_aturi); 269 361 let ast = engine 270 362 .compile_file(PathBuf::from_str(source)?) 271 363 .context("cannot compile script")?; ··· 278 370 } 279 371 280 372 impl Matcher for RhaiMatcher { 281 - fn matches(&self, value: &serde_json::Value) -> bool { 373 + fn matches(&self, value: &serde_json::Value) -> Result<MatcherResult> { 282 374 let mut scope = Scope::new(); 283 375 let value_map = to_dynamic(value); 284 376 if let Err(err) = value_map { 285 - println!("error: {:?}", err); 286 377 tracing::error!(source = ?self.source, error = ?err, "error converting value to dynamic"); 287 - return false; 378 + return Ok(MatcherResult::default()); 288 379 } 289 380 let value_map = value_map.unwrap(); 290 381 scope.push("event", value_map); 291 382 292 - let result = self 293 - .engine 294 - .eval_ast_with_scope::<MatcherResult>(&mut scope, &self.ast); 383 + self.engine 384 + .eval_ast_with_scope::<MatcherResult>(&mut scope, &self.ast) 385 + .context("error evaluating script") 386 + } 387 + } 388 + 389 + fn build_aturi_maybe(event: Dynamic) -> Result<String> { 390 + println!("{event:?}"); 391 + let event = event.as_map_ref().map_err(|err| anyhow!(err))?; 295 392 296 - if let Err(err) = result { 297 - println!("error: {:?}", err); 298 - tracing::error!(source = ?self.source, error = ?err, "error evaluating script"); 299 - return false; 300 - } 393 + let commit = event.get("commit").ok_or(anyhow!("no commit on event"))?.as_map_ref().map_err(|err| anyhow!(err))?; 394 + let record = commit.get("record").ok_or(anyhow!("no record on event commit"))?.as_map_ref().map_err(|err| anyhow!(err))?; 301 395 302 - let result = result.unwrap(); 396 + let rtype = record.get("$type").ok_or(anyhow!("no $type on event commit record"))?.as_immutable_string_ref().map_err(|err| anyhow!(err))?; 303 397 304 - result.matched 398 + if rtype.as_str() == "app.bsky.feed.post" { 399 + let did = event.get("did").ok_or(anyhow!("no did on event"))?.as_immutable_string_ref().map_err(|err| anyhow!(err))?; 400 + let collection = commit.get("collection").ok_or(anyhow!("no collection on event"))?.as_immutable_string_ref().map_err(|err| anyhow!(err))?; 401 + let rkey = commit.get("rkey").ok_or(anyhow!("no rkey on event commit"))?.as_immutable_string_ref().map_err(|err| anyhow!(err))?; 402 + 403 + return Ok(format!("at://{}/{}/{}", did.as_str(), collection.as_str(), rkey.as_str())); 305 404 } 405 + 406 + 407 + Err(anyhow!("no aturi for event")) 408 + } 409 + 410 + fn build_aturi(event: Dynamic) -> String { 411 + let aturi = build_aturi_maybe(event); 412 + if let Err(err) = aturi { 413 + println!("error {err:?}"); 414 + return "".into(); 415 + } 416 + aturi.unwrap() 306 417 } 307 418 } 308 419 ··· 348 459 ]; 349 460 350 461 for (path, expected, result) in tests { 351 - let matcher = EqualsMatcher::new(expected, path).expect("matcher is valid"); 352 - assert_eq!(matcher.matches(&value), result); 462 + let matcher = EqualsMatcher::new(expected, path, &None).expect("matcher is valid"); 463 + assert_eq!(matcher.matches(&value).expect("match ok"), result); 353 464 } 354 465 } 355 466 ··· 396 507 ]; 397 508 398 509 for (path, prefix, result) in tests { 399 - let matcher = PrefixMatcher::new(prefix, path).expect("matcher is valid"); 400 - assert_eq!(matcher.matches(&value), result); 510 + let matcher = PrefixMatcher::new(prefix, path, &None).expect("matcher is valid"); 511 + assert_eq!(matcher.matches(&value).expect("match ok"), result); 401 512 } 402 513 } 403 514 ··· 463 574 ]; 464 575 465 576 for (path, values, result) in tests { 466 - let matcher = SequenceMatcher::new(&values, path).expect("matcher is valid"); 467 - assert_eq!(matcher.matches(&value), result); 577 + let matcher = SequenceMatcher::new(&values, path, &None).expect("matcher is valid"); 578 + assert_eq!(matcher.matches(&value).expect("match ok"), result); 468 579 } 469 580 } 470 581 ··· 472 583 fn sequence_matcher_edge_case_1() { 473 584 let raw_json = r#"{"text": "Stellwerkstörung. Und Signalstörung. Und der Alternativzug ist auch ausgefallen. Und überhaupt."}"#; 474 585 let value: serde_json::Value = serde_json::from_str(raw_json).expect("json is valid"); 475 - let matcher = 476 - SequenceMatcher::new(&vec!["smoke".to_string(), "signal".to_string()], "$.text") 477 - .expect("matcher is valid"); 478 - assert_eq!(matcher.matches(&value), false); 586 + let matcher = SequenceMatcher::new( 587 + &vec!["smoke".to_string(), "signal".to_string()], 588 + "$.text", 589 + &None, 590 + ) 591 + .expect("matcher is valid"); 592 + assert_eq!(matcher.matches(&value).expect("match ok"), false); 479 593 } 480 594 } 481 595 482 596 #[cfg(all(test, feature = "rhai"))] 483 597 mod rhaitests { 484 598 485 - use anyhow::{anyhow, Result}; 486 599 use super::rhai::*; 487 600 use super::*; 601 + use anyhow::{anyhow, Result}; 488 602 use std::path::PathBuf; 489 603 490 604 #[cfg(feature = "rhai")] 491 605 #[test] 492 606 fn rhai_matcher() -> Result<()> { 493 - 494 607 let testdata = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata"); 495 608 496 609 let tests = vec![ 497 - ("post1.json", [("rhai_match_everything.rhai", true),("rhai_match_type.rhai", true),("rhai_match_poster.rhai", true), ("rhai_match_reply_root.rhai", false)]), 498 - ("post2.json", [("rhai_match_everything.rhai", true),("rhai_match_type.rhai", true),("rhai_match_poster.rhai", true), ("rhai_match_reply_root.rhai", true)]) 610 + ( 611 + "post1.json", 612 + [ 613 + ("rhai_match_everything.rhai", true, "at://did:plc:cbkjy5n7bk3ax2wplmtjofq2/app.bsky.feed.post/3laadb7behk25"), 614 + ("rhai_match_type.rhai", true, "at://did:plc:cbkjy5n7bk3ax2wplmtjofq2/app.bsky.feed.post/3laadb7behk25"), 615 + ("rhai_match_poster.rhai", true, "at://did:plc:cbkjy5n7bk3ax2wplmtjofq2/app.bsky.feed.post/3laadb7behk25"), 616 + ("rhai_match_reply_root.rhai", false, ""), 617 + ], 618 + ), 619 + ( 620 + "post2.json", 621 + [ 622 + ("rhai_match_everything.rhai", true, "at://did:plc:cbkjy5n7bk3ax2wplmtjofq2/app.bsky.feed.post/3laadftr72k25"), 623 + ("rhai_match_type.rhai", true, "at://did:plc:cbkjy5n7bk3ax2wplmtjofq2/app.bsky.feed.post/3laadftr72k25"), 624 + ("rhai_match_poster.rhai", true, "at://did:plc:cbkjy5n7bk3ax2wplmtjofq2/app.bsky.feed.post/3laadftr72k25"), 625 + ("rhai_match_reply_root.rhai", true, "at://did:plc:cbkjy5n7bk3ax2wplmtjofq2/app.bsky.feed.post/3laadftr72k25"), 626 + ], 627 + ), 499 628 ]; 500 629 501 630 for (input_json, matcher_tests) in tests { ··· 503 632 let json_content = std::fs::read(input_json_path).map_err(|err| { 504 633 anyhow::Error::new(err).context(anyhow!("reading input_json failed")) 505 634 })?; 506 - let value: serde_json::Value = serde_json::from_slice(&json_content).context("parsing input_json failed")?; 635 + let value: serde_json::Value = 636 + serde_json::from_slice(&json_content).context("parsing input_json failed")?; 507 637 508 - for (matcher_file_name, expected) in matcher_tests { 638 + for (matcher_file_name, matched, aturi) in matcher_tests { 509 639 let matcher_path = testdata.join(matcher_file_name); 510 - let matcher = RhaiMatcher::new(&matcher_path.to_string_lossy()).context("could not construct matcher")?; 511 - assert_eq!(matcher.matches(&value), expected); 640 + let matcher = RhaiMatcher::new(&matcher_path.to_string_lossy()) 641 + .context("could not construct matcher")?; 642 + let result = matcher.matches(&value)?; 643 + assert_eq!(result.matched, matched, "matched {}: {}", input_json, matcher_file_name); 644 + assert_eq!(result.aturi, aturi, "aturi {}: {}", input_json, matcher_file_name); 512 645 } 513 - 514 646 } 515 647 516 648 Ok(())
+4 -1
testdata/rhai_match_everything.rhai
··· 1 - 2 1 let result = new_matcher_result(); 3 2 result.matched = true; 3 + 4 + if result.matched { 5 + result.aturi = build_aturi(event); 6 + } 4 7 5 8 result
+4
testdata/rhai_match_poster.rhai
··· 10 10 _ => { } 11 11 } 12 12 13 + if result.matched { 14 + result.aturi = build_aturi(event); 15 + } 16 + 13 17 result
+5 -2
testdata/rhai_match_reply_root.rhai
··· 1 - 2 1 let result = new_matcher_result(); 3 2 4 3 let rtype = event?.commit?.record["$type"]; ··· 9 8 10 9 let root_uri = event?.commit?.record?.reply?.root?.uri; 11 10 12 - result.matched = root_uri.starts_with("at://did:plc:cbkjy5n7bk3ax2wplmtjofq2/app.bsky.feed.post/"); 11 + result.matched = `${root_uri}`.starts_with("at://did:plc:cbkjy5n7bk3ax2wplmtjofq2/app.bsky.feed.post/"); 12 + 13 + if result.matched { 14 + result.aturi = build_aturi(event); 15 + } 13 16 14 17 result
+5
testdata/rhai_match_type.rhai
··· 11 11 _ => { } 12 12 } 13 13 14 + if result.matched { 15 + result.aturi = build_aturi(event); 16 + } 17 + 18 + 14 19 result