A new crate, bgp-policy, runs functions of a script on routes: on their
way in, after the general checks of a peer's import and before the
checks that can change (RPKI, next hop reachability); and on their way
out to a peer. A function is given the route and the peer, may refuse
the route, and may change its communities, large communities,
LOCAL_PREF and MED, and on the way out prepend our AS:
fn from_peer(route, peer) {
if route.has_community("65535:666") { return false; }
route.add_community(LEARNED_FROM[peer.role]);
route.local_pref = PREFERENCE[peer.role];
true
}
Configuration: policy { path | source, import_function, export_function }
and, for a peer, import { function } and export { function }. The script
is compiled at startup; `bgp --check` reads it and says which function
is whose. A function that fails on a route has refused it. Scripts have
no files, network or clock, and are stopped after 200,000 steps.
Route selection now goes by a LOCAL_PREF on a route from an external
peer: the decoder never lets a peer's own through, so one that is there
was set by policy.
What it costs was measured before it was built on (benches/policy.rs):
a call that tests a few communities and changes two takes 1 to 3
microseconds, some two hundred times what Rust would. So a function is
asked once for the routes of an UPDATE, which share their attributes,
unless it asks for the prefix. One peer sending 1.23 M routes over a
session: 0.37 s without a function, 0.80 s with one that marks and
ranks every route, 3.4 s if it also looks at the prefix.
Export applies to what is announced, which today is what the daemon
originates; learned routes are not passed on yet.
Tested by unit tests of the crate, sessions end to end, a fuzz target on
the example script (testdata/policy.rhai), and a lab phase in which a
script changes which of FRR's and BIRD's route is chosen and what each
of them is told.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
630 lines
22 KiB
Rust
630 lines
22 KiB
Rust
//! What a policy function is given: the route and the peer.
|
|
//!
|
|
//! # The route
|
|
//!
|
|
//! | In a script | |
|
|
//! |---|---|
|
|
//! | `route.prefix` | `"203.0.113.0/24"` |
|
|
//! | `route.prefix_len` | `24` |
|
|
//! | `route.within("203.0.112.0/22")` | whether the prefix is that one or inside it |
|
|
//! | `route.family` | `"ipv4"` or `"ipv6"` |
|
|
//! | `route.as_path` | `[64602, 3356, 15169]`, the neighbor's AS first |
|
|
//! | `route.as_path_len` | as route selection counts it: a set is one |
|
|
//! | `route.origin_as`, `route.neighbor_as` | the last and the first AS of the path; 0 if there is none |
|
|
//! | `route.path_has(3356)` | whether the AS is anywhere on the path |
|
|
//! | `route.origin` | `"igp"`, `"egp"` or `"incomplete"` |
|
|
//! | `route.next_hop` | `"192.0.2.1"` |
|
|
//! | `route.med`, `route.local_pref` | the number, or `()` if the route has none; both can be set, and unset with `()` |
|
|
//! | `route.communities` | `["3356:3", "3356:2012"]` |
|
|
//! | `route.has_community("3356:3")` | also `(3356, 3)`, and `"3356:*"` for any of that AS |
|
|
//! | `route.add_community("64601:100")` | also `(64601, 100)` |
|
|
//! | `route.remove_community("3356:*")` | all that match; `"*:*"` for all there are |
|
|
//! | `route.large_communities`, `route.has_large_community("64601:1:*")`, `route.add_large_community(..)`, `route.remove_large_community(..)` | the same for large communities (RFC 8092) |
|
|
//! | `route.prepend(2)` | on the way out: our AS that many more times on the path |
|
|
//!
|
|
//! # The peer
|
|
//!
|
|
//! `peer.name` (as configured), `peer.asn`, `peer.address`, `peer.role`
|
|
//! (`"customer"`, `"provider"`, `"peer"`, `"route_server"`,
|
|
//! `"route_server_client"`, or `""` if none is configured), `peer.ibgp`,
|
|
//! and `peer.local_asn`, which is our own.
|
|
|
|
use std::net::IpAddr;
|
|
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
|
|
|
|
use bgp_packet::community::{format_large, format_standard};
|
|
use bgp_packet::message::{
|
|
AsPathAttribute, AsPathSegment, CommunitiesPathAttribute, LargeCommunitiesPathAttribute,
|
|
LargeCommunity, LocalPrefPathAttribute, MultiExitDiscPathAttribute, NlriNextHop,
|
|
OriginPathAttribute, PathAttribute,
|
|
};
|
|
use bgp_primitives::constants::IpVersion;
|
|
use bgp_primitives::ip_prefix::IpPrefix;
|
|
use rhai::{Array, Dynamic, Engine, EvalAltResult, INT, ImmutableString};
|
|
|
|
use crate::{Direction, Subject};
|
|
|
|
type Fallible<T> = Result<T, Box<EvalAltResult>>;
|
|
|
|
/// A peer, as a script sees it. Made once for a session.
|
|
#[derive(Debug, Clone)]
|
|
pub struct Peer {
|
|
name: ImmutableString,
|
|
address: ImmutableString,
|
|
role: ImmutableString,
|
|
asn: u32,
|
|
local_asn: u32,
|
|
ibgp: bool,
|
|
}
|
|
|
|
impl Peer {
|
|
/// `role` is the peer's role by the name scripts know it under, or
|
|
/// empty.
|
|
pub fn new(name: &str, address: IpAddr, asn: u32, local_asn: u32, role: &str) -> Self {
|
|
Self {
|
|
name: name.into(),
|
|
address: address.to_string().into(),
|
|
role: role.into(),
|
|
asn,
|
|
local_asn,
|
|
ibgp: asn == local_asn,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The route a function is looking at, and what it has done to it.
|
|
#[derive(Debug)]
|
|
struct State {
|
|
prefix: IpPrefix,
|
|
direction: Direction,
|
|
local_asn: u32,
|
|
ibgp: bool,
|
|
/// The AS numbers of the path in order, sets and all.
|
|
as_path: Vec<u32>,
|
|
as_path_len: usize,
|
|
origin_as: u32,
|
|
neighbor_as: u32,
|
|
origin: OriginPathAttribute,
|
|
next_hop: Option<NlriNextHop>,
|
|
communities: Vec<(u16, u16)>,
|
|
large_communities: Vec<LargeCommunity>,
|
|
local_pref: Option<u32>,
|
|
med: Option<u32>,
|
|
/// How many more times our AS is to go on the path.
|
|
prepend: usize,
|
|
saw_prefix: bool,
|
|
/// Which of the above the function changed.
|
|
changed: Changed,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, Copy)]
|
|
struct Changed {
|
|
communities: bool,
|
|
large_communities: bool,
|
|
local_pref: bool,
|
|
med: bool,
|
|
}
|
|
|
|
/// The handle a script holds. The function is given a copy and changes
|
|
/// what it points to; the caller reads that afterwards.
|
|
#[derive(Debug, Clone)]
|
|
pub(crate) struct Route(Arc<Mutex<State>>);
|
|
|
|
impl Route {
|
|
pub(crate) fn new(subject: &Subject<'_>, peer: &Peer) -> Self {
|
|
let mut state = State {
|
|
prefix: *subject.prefix,
|
|
direction: subject.direction,
|
|
local_asn: peer.local_asn,
|
|
ibgp: peer.ibgp,
|
|
as_path: Vec::new(),
|
|
as_path_len: 0,
|
|
origin_as: 0,
|
|
neighbor_as: 0,
|
|
origin: OriginPathAttribute::INCOMPLETE,
|
|
next_hop: subject.next_hop,
|
|
communities: Vec::new(),
|
|
large_communities: Vec::new(),
|
|
local_pref: None,
|
|
med: None,
|
|
prepend: 0,
|
|
saw_prefix: false,
|
|
changed: Changed::default(),
|
|
};
|
|
for attribute in subject.attributes {
|
|
match attribute {
|
|
PathAttribute::Origin(origin) => state.origin = *origin,
|
|
PathAttribute::AsPath(path) => {
|
|
state.as_path_len = path.hops();
|
|
let first = path.segments.first().filter(|segment| segment.ordered);
|
|
let last = path.segments.last().filter(|segment| segment.ordered);
|
|
state.neighbor_as = first.and_then(|s| s.path.first()).copied().unwrap_or(0);
|
|
state.origin_as = last.and_then(|s| s.path.last()).copied().unwrap_or(0);
|
|
state.as_path = path
|
|
.segments
|
|
.iter()
|
|
.flat_map(|segment| segment.path.iter().copied())
|
|
.collect();
|
|
}
|
|
PathAttribute::NextHop(next_hop) => {
|
|
state.next_hop = Some(NlriNextHop::Ipv4(next_hop.0));
|
|
}
|
|
PathAttribute::MultiExitDisc(med) => state.med = Some(med.0),
|
|
PathAttribute::LocalPref(local_pref) => state.local_pref = Some(local_pref.0),
|
|
PathAttribute::Communities(communities) => {
|
|
state.communities.clone_from(&communities.0);
|
|
}
|
|
PathAttribute::LargeCommunities(large) => {
|
|
state.large_communities.clone_from(&large.communities);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
Self(Arc::new(Mutex::new(state)))
|
|
}
|
|
|
|
fn state(&self) -> MutexGuard<'_, State> {
|
|
self.0.lock().unwrap_or_else(PoisonError::into_inner)
|
|
}
|
|
|
|
/// What the function left: `attributes` as changed, if it changed
|
|
/// any, and whether it asked for the prefix.
|
|
pub(crate) fn finish(
|
|
&self,
|
|
attributes: &[PathAttribute],
|
|
) -> (Option<Vec<PathAttribute>>, bool) {
|
|
let state = self.state();
|
|
let changed = state.changed;
|
|
let prepend = state.prepend > 0 && !state.ibgp;
|
|
let any = changed.communities
|
|
|| changed.large_communities
|
|
|| changed.local_pref
|
|
|| changed.med
|
|
|| prepend;
|
|
if !any {
|
|
return (None, state.saw_prefix);
|
|
}
|
|
let mut attributes = attributes.to_vec();
|
|
if changed.communities {
|
|
let communities = (!state.communities.is_empty()).then(|| {
|
|
PathAttribute::Communities(CommunitiesPathAttribute(state.communities.clone()))
|
|
});
|
|
put(
|
|
&mut attributes,
|
|
bgp_packet::message::attr_type::COMMUNITIES,
|
|
communities,
|
|
);
|
|
}
|
|
if changed.large_communities {
|
|
let large = (!state.large_communities.is_empty()).then(|| {
|
|
PathAttribute::LargeCommunities(LargeCommunitiesPathAttribute {
|
|
communities: state.large_communities.clone(),
|
|
})
|
|
});
|
|
put(
|
|
&mut attributes,
|
|
bgp_packet::message::attr_type::LARGE_COMMUNITIES,
|
|
large,
|
|
);
|
|
}
|
|
if changed.local_pref {
|
|
let local_pref = state
|
|
.local_pref
|
|
.map(|value| PathAttribute::LocalPref(LocalPrefPathAttribute(value)));
|
|
put(
|
|
&mut attributes,
|
|
bgp_packet::message::attr_type::LOCAL_PREF,
|
|
local_pref,
|
|
);
|
|
}
|
|
if changed.med {
|
|
let med = state
|
|
.med
|
|
.map(|value| PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(value)));
|
|
put(
|
|
&mut attributes,
|
|
bgp_packet::message::attr_type::MULTI_EXIT_DISC,
|
|
med,
|
|
);
|
|
}
|
|
if prepend {
|
|
let ours = std::iter::repeat_n(state.local_asn, state.prepend);
|
|
for attribute in &mut attributes {
|
|
if let PathAttribute::AsPath(path) = attribute {
|
|
prepend_to(path, ours);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
(Some(attributes), state.saw_prefix)
|
|
}
|
|
}
|
|
|
|
/// Puts `ours` at the front of `path`.
|
|
fn prepend_to(path: &mut AsPathAttribute, ours: impl Iterator<Item = u32>) {
|
|
match path.segments.first_mut() {
|
|
Some(first) if first.ordered => {
|
|
first.path.splice(0..0, ours);
|
|
}
|
|
_ => path.segments.insert(
|
|
0,
|
|
AsPathSegment {
|
|
ordered: true,
|
|
path: ours.collect(),
|
|
},
|
|
),
|
|
}
|
|
}
|
|
|
|
/// Makes the attribute of type `type_code` in `attributes` be `with`:
|
|
/// replaced, added in the order of the type codes, or taken out.
|
|
fn put(attributes: &mut Vec<PathAttribute>, type_code: u8, with: Option<PathAttribute>) {
|
|
let at = attributes
|
|
.iter()
|
|
.position(|attribute| attribute.type_code() == type_code);
|
|
match (at, with) {
|
|
(Some(at), Some(with)) => attributes[at] = with,
|
|
(Some(at), None) => {
|
|
attributes.remove(at);
|
|
}
|
|
(None, Some(with)) => {
|
|
let after = attributes
|
|
.iter()
|
|
.position(|attribute| attribute.type_code() > type_code)
|
|
.unwrap_or(attributes.len());
|
|
attributes.insert(after, with);
|
|
}
|
|
(None, None) => {}
|
|
}
|
|
}
|
|
|
|
fn wrong(what: impl Into<String>) -> Box<EvalAltResult> {
|
|
what.into().into()
|
|
}
|
|
|
|
/// A number a script gave, as one that fits where it is going.
|
|
fn number<T: TryFrom<INT>>(value: INT, what: &str) -> Fallible<T> {
|
|
T::try_from(value).map_err(|_| wrong(format!("{value} is not {what}")))
|
|
}
|
|
|
|
/// One part of a community as a script writes it: a number, or `*` for
|
|
/// any.
|
|
fn part<T: std::str::FromStr>(text: &str, whole: &str) -> Fallible<Option<T>> {
|
|
if text == "*" {
|
|
return Ok(None);
|
|
}
|
|
text.parse()
|
|
.map(Some)
|
|
.map_err(|_| wrong(format!("{whole:?} is not a community")))
|
|
}
|
|
|
|
type Pattern = (Option<u16>, Option<u16>);
|
|
type LargePattern = (Option<u32>, Option<u32>, Option<u32>);
|
|
|
|
fn pattern(text: &str) -> Fallible<Pattern> {
|
|
let (high, low) = text.split_once(':').ok_or_else(|| {
|
|
wrong(format!(
|
|
"{text:?} is not a community: it is written 64601:100"
|
|
))
|
|
})?;
|
|
Ok((part(high, text)?, part(low, text)?))
|
|
}
|
|
|
|
fn large_pattern(text: &str) -> Fallible<LargePattern> {
|
|
let mut parts = text.splitn(3, ':');
|
|
let mut next = || {
|
|
parts.next().ok_or_else(|| {
|
|
wrong(format!(
|
|
"{text:?} is not a large community: it is written 64601:1:2"
|
|
))
|
|
})
|
|
};
|
|
let (admin, first, second) = (next()?, next()?, next()?);
|
|
Ok((part(admin, text)?, part(first, text)?, part(second, text)?))
|
|
}
|
|
|
|
/// A pattern with nothing left open: a community to add.
|
|
fn exact((high, low): Pattern, text: &str) -> Fallible<(u16, u16)> {
|
|
high.zip(low).ok_or_else(|| {
|
|
wrong(format!(
|
|
"{text:?} cannot be added: it stands for more than one"
|
|
))
|
|
})
|
|
}
|
|
|
|
fn exact_large((admin, first, second): LargePattern, text: &str) -> Fallible<LargeCommunity> {
|
|
match (admin, first, second) {
|
|
(Some(global_admin), Some(data_1), Some(data_2)) => Ok(LargeCommunity {
|
|
global_admin,
|
|
data_1,
|
|
data_2,
|
|
}),
|
|
_ => Err(wrong(format!(
|
|
"{text:?} cannot be added: it stands for more than one"
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn fits<T: PartialEq>(wanted: Option<T>, has: T) -> bool {
|
|
wanted.is_none_or(|wanted| wanted == has)
|
|
}
|
|
|
|
fn matches((high, low): Pattern, community: (u16, u16)) -> bool {
|
|
fits(high, community.0) && fits(low, community.1)
|
|
}
|
|
|
|
fn matches_large((admin, first, second): LargePattern, community: &LargeCommunity) -> bool {
|
|
fits(admin, community.global_admin)
|
|
&& fits(first, community.data_1)
|
|
&& fits(second, community.data_2)
|
|
}
|
|
|
|
impl State {
|
|
fn add_community(&mut self, community: (u16, u16)) {
|
|
if !self.communities.contains(&community) {
|
|
self.communities.push(community);
|
|
self.changed.communities = true;
|
|
}
|
|
}
|
|
|
|
fn remove_communities(&mut self, pattern: Pattern) {
|
|
let before = self.communities.len();
|
|
self.communities
|
|
.retain(|community| !matches(pattern, *community));
|
|
self.changed.communities |= self.communities.len() != before;
|
|
}
|
|
|
|
fn set_local_pref(&mut self, value: Option<u32>) {
|
|
self.changed.local_pref |= self.local_pref != value;
|
|
self.local_pref = value;
|
|
}
|
|
|
|
fn set_med(&mut self, value: Option<u32>) {
|
|
self.changed.med |= self.med != value;
|
|
self.med = value;
|
|
}
|
|
}
|
|
|
|
fn optional(value: Option<u32>) -> Dynamic {
|
|
value.map_or(Dynamic::UNIT, |value| Dynamic::from(INT::from(value)))
|
|
}
|
|
|
|
/// Tells `engine` what a route and a peer are, and what can be asked of
|
|
/// them.
|
|
pub(crate) fn register(engine: &mut Engine) {
|
|
engine.register_type_with_name::<Route>("Route");
|
|
engine.register_type_with_name::<Peer>("Peer");
|
|
|
|
// The peer.
|
|
engine.register_get("name", |peer: &mut Peer| peer.name.clone());
|
|
engine.register_get("address", |peer: &mut Peer| peer.address.clone());
|
|
engine.register_get("role", |peer: &mut Peer| peer.role.clone());
|
|
engine.register_get("asn", |peer: &mut Peer| INT::from(peer.asn));
|
|
engine.register_get("local_asn", |peer: &mut Peer| INT::from(peer.local_asn));
|
|
engine.register_get("ibgp", |peer: &mut Peer| peer.ibgp);
|
|
|
|
// The prefix. Asking for it is noted: the answer to a function that
|
|
// did not is good for every prefix.
|
|
engine.register_get("prefix", |route: &mut Route| {
|
|
let mut state = route.state();
|
|
state.saw_prefix = true;
|
|
state.prefix.to_string()
|
|
});
|
|
engine.register_get("prefix_len", |route: &mut Route| {
|
|
let mut state = route.state();
|
|
state.saw_prefix = true;
|
|
INT::from(state.prefix.length())
|
|
});
|
|
engine.register_fn(
|
|
"within",
|
|
|route: &mut Route, around: &str| -> Fallible<bool> {
|
|
let around = IpPrefix::try_from(around)
|
|
.map_err(|e| wrong(format!("{around:?} is not a prefix: {e}")))?;
|
|
let mut state = route.state();
|
|
state.saw_prefix = true;
|
|
Ok(around.contains(&state.prefix))
|
|
},
|
|
);
|
|
// Every route of an UPDATE's one batch is of the same family.
|
|
engine.register_get("family", |route: &mut Route| {
|
|
match route.state().prefix.address_family() {
|
|
IpVersion::Ipv4 => "ipv4",
|
|
IpVersion::Ipv6 => "ipv6",
|
|
}
|
|
});
|
|
|
|
// The path.
|
|
engine.register_get("as_path", |route: &mut Route| -> Array {
|
|
route
|
|
.state()
|
|
.as_path
|
|
.iter()
|
|
.map(|asn| Dynamic::from(INT::from(*asn)))
|
|
.collect()
|
|
});
|
|
engine.register_get("as_path_len", |route: &mut Route| {
|
|
route.state().as_path_len as INT
|
|
});
|
|
engine.register_get("origin_as", |route: &mut Route| {
|
|
INT::from(route.state().origin_as)
|
|
});
|
|
engine.register_get("neighbor_as", |route: &mut Route| {
|
|
INT::from(route.state().neighbor_as)
|
|
});
|
|
engine.register_fn("path_has", |route: &mut Route, asn: INT| {
|
|
u32::try_from(asn).is_ok_and(|asn| route.state().as_path.contains(&asn))
|
|
});
|
|
engine.register_fn("prepend", |route: &mut Route, times: INT| -> Fallible<()> {
|
|
let mut state = route.state();
|
|
if state.direction == Direction::Import {
|
|
return Err(wrong(
|
|
"prepend is for routes on their way out; to make a route less preferred on \
|
|
its way in, set its local_pref",
|
|
));
|
|
}
|
|
// More than this is a mistake, and makes the route no worse.
|
|
state.prepend = (0..=16)
|
|
.contains(×)
|
|
.then_some(times as usize)
|
|
.ok_or_else(|| wrong(format!("prepend({times}): from 0 to 16 times")))?;
|
|
Ok(())
|
|
});
|
|
|
|
// The other attributes that are looked at.
|
|
engine.register_get("origin", |route: &mut Route| match route.state().origin {
|
|
OriginPathAttribute::IGP => "igp",
|
|
OriginPathAttribute::EGP => "egp",
|
|
OriginPathAttribute::INCOMPLETE => "incomplete",
|
|
});
|
|
engine.register_get("next_hop", |route: &mut Route| {
|
|
route
|
|
.state()
|
|
.next_hop
|
|
.map_or_else(String::new, |next_hop| match next_hop {
|
|
NlriNextHop::Ipv4(address) => address.to_string(),
|
|
NlriNextHop::Ipv6(address) => address.to_string(),
|
|
NlriNextHop::Ipv6WithLl { global, .. } => global.to_string(),
|
|
})
|
|
});
|
|
|
|
// Those that are also set.
|
|
engine.register_get("local_pref", |route: &mut Route| {
|
|
optional(route.state().local_pref)
|
|
});
|
|
engine.register_set(
|
|
"local_pref",
|
|
|route: &mut Route, value: INT| -> Fallible<()> {
|
|
let value = number(value, "a LOCAL_PREF")?;
|
|
route.state().set_local_pref(Some(value));
|
|
Ok(())
|
|
},
|
|
);
|
|
engine.register_set("local_pref", |route: &mut Route, (): ()| {
|
|
route.state().set_local_pref(None);
|
|
});
|
|
engine.register_get("med", |route: &mut Route| optional(route.state().med));
|
|
engine.register_set("med", |route: &mut Route, value: INT| -> Fallible<()> {
|
|
let value = number(value, "a MULTI_EXIT_DISC")?;
|
|
route.state().set_med(Some(value));
|
|
Ok(())
|
|
});
|
|
engine.register_set("med", |route: &mut Route, (): ()| {
|
|
route.state().set_med(None);
|
|
});
|
|
|
|
// Communities.
|
|
engine.register_get("communities", |route: &mut Route| -> Array {
|
|
route
|
|
.state()
|
|
.communities
|
|
.iter()
|
|
.map(|community| Dynamic::from(format_standard(*community)))
|
|
.collect()
|
|
});
|
|
engine.register_fn(
|
|
"has_community",
|
|
|route: &mut Route, text: &str| -> Fallible<bool> {
|
|
let wanted = pattern(text)?;
|
|
Ok(route
|
|
.state()
|
|
.communities
|
|
.iter()
|
|
.any(|community| matches(wanted, *community)))
|
|
},
|
|
);
|
|
engine.register_fn(
|
|
"has_community",
|
|
|route: &mut Route, high: INT, low: INT| -> Fallible<bool> {
|
|
let wanted = (
|
|
number(high, "half a community")?,
|
|
number(low, "half a community")?,
|
|
);
|
|
Ok(route.state().communities.contains(&wanted))
|
|
},
|
|
);
|
|
engine.register_fn(
|
|
"add_community",
|
|
|route: &mut Route, text: &str| -> Fallible<()> {
|
|
let community = exact(pattern(text)?, text)?;
|
|
route.state().add_community(community);
|
|
Ok(())
|
|
},
|
|
);
|
|
engine.register_fn(
|
|
"add_community",
|
|
|route: &mut Route, high: INT, low: INT| -> Fallible<()> {
|
|
let community = (
|
|
number(high, "half a community")?,
|
|
number(low, "half a community")?,
|
|
);
|
|
route.state().add_community(community);
|
|
Ok(())
|
|
},
|
|
);
|
|
engine.register_fn(
|
|
"remove_community",
|
|
|route: &mut Route, text: &str| -> Fallible<()> {
|
|
let unwanted = pattern(text)?;
|
|
route.state().remove_communities(unwanted);
|
|
Ok(())
|
|
},
|
|
);
|
|
engine.register_fn(
|
|
"remove_community",
|
|
|route: &mut Route, high: INT, low: INT| -> Fallible<()> {
|
|
let unwanted = (
|
|
Some(number(high, "half a community")?),
|
|
Some(number(low, "half a community")?),
|
|
);
|
|
route.state().remove_communities(unwanted);
|
|
Ok(())
|
|
},
|
|
);
|
|
|
|
// Large communities.
|
|
engine.register_get("large_communities", |route: &mut Route| -> Array {
|
|
route
|
|
.state()
|
|
.large_communities
|
|
.iter()
|
|
.map(|community| Dynamic::from(format_large(community)))
|
|
.collect()
|
|
});
|
|
engine.register_fn(
|
|
"has_large_community",
|
|
|route: &mut Route, text: &str| -> Fallible<bool> {
|
|
let wanted = large_pattern(text)?;
|
|
Ok(route
|
|
.state()
|
|
.large_communities
|
|
.iter()
|
|
.any(|community| matches_large(wanted, community)))
|
|
},
|
|
);
|
|
engine.register_fn(
|
|
"add_large_community",
|
|
|route: &mut Route, text: &str| -> Fallible<()> {
|
|
let community = exact_large(large_pattern(text)?, text)?;
|
|
let mut state = route.state();
|
|
if !state.large_communities.contains(&community) {
|
|
state.large_communities.push(community);
|
|
state.changed.large_communities = true;
|
|
}
|
|
Ok(())
|
|
},
|
|
);
|
|
engine.register_fn(
|
|
"remove_large_community",
|
|
|route: &mut Route, text: &str| -> Fallible<()> {
|
|
let unwanted = large_pattern(text)?;
|
|
let mut state = route.state();
|
|
let before = state.large_communities.len();
|
|
state
|
|
.large_communities
|
|
.retain(|community| !matches_large(unwanted, community));
|
|
state.changed.large_communities |= state.large_communities.len() != before;
|
|
Ok(())
|
|
},
|
|
);
|
|
}
|