Skip to main content

graphrefly/
composition.rs

1//! Graph-layer composition helpers (B70 / D56).
2//!
3//! These are per-language sugar over declared graph nodes. They do not add
4//! protocol verbs, live topology streams, or dynamic branch lifecycles.
5
6use std::collections::{BTreeMap, BTreeSet};
7use std::rc::Rc;
8
9use crate::ctx::{Ctx, DepTerminal};
10use crate::graph::{Graph, GraphNodeOpts};
11use crate::node::{Node, NodeOpts};
12use crate::operators::Operator;
13use crate::protocol::Message;
14
15/// Rust-native unary operator composition builder.
16pub struct Pipe<T> {
17    graph: Graph,
18    current: Node<T>,
19}
20
21/// Creates or computes `pipe`.
22pub fn pipe<T: 'static>(graph: &Graph, source: Node<T>) -> Pipe<T> {
23    Pipe {
24        graph: graph.clone(),
25        current: source,
26    }
27}
28
29impl<T: 'static> Pipe<T> {
30    /// Updates or reads `through`.
31    pub fn through<U: 'static>(self, op: Operator<U>) -> Pipe<U> {
32        self.through_opts(op, GraphNodeOpts::default())
33    }
34
35    /// Updates or reads `through_opts`.
36    pub fn through_opts<U: 'static>(self, op: Operator<U>, opts: GraphNodeOpts) -> Pipe<U> {
37        let current = self.graph.init_node(op, vec![self.current.erased()], opts);
38        Pipe {
39            graph: self.graph,
40            current,
41        }
42    }
43
44    /// Updates or reads `done`.
45    pub fn done(self) -> Node<T> {
46        self.current
47    }
48}
49
50#[derive(Clone, Debug, PartialEq, Eq)]
51/// `StratifyRule` data container.
52pub struct StratifyRule<R> {
53    /// `name` field for name.
54    pub name: String,
55    /// `rule` field for rule.
56    pub rule: R,
57    /// `meta` field for meta.
58    pub meta: BTreeMap<String, String>,
59}
60
61impl<R> StratifyRule<R> {
62    /// Creates or computes `new`.
63    pub fn new(name: impl Into<String>, rule: R) -> Self {
64        Self {
65            name: name.into(),
66            rule,
67            meta: BTreeMap::new(),
68        }
69    }
70
71    /// Updates or reads `meta`.
72    pub fn meta(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
73        self.meta.insert(key.into(), value.into());
74        self
75    }
76}
77
78#[derive(Clone, Debug)]
79/// `StratifyOptions` data container.
80pub struct StratifyOptions {
81    /// `prefix` field for prefix.
82    pub prefix: String,
83    /// `rules` field for rules.
84    pub rules: GraphNodeOpts,
85    /// `branches` field for branches.
86    pub branches: BTreeMap<String, GraphNodeOpts>,
87}
88
89impl Default for StratifyOptions {
90    fn default() -> Self {
91        Self {
92            prefix: "branch".to_owned(),
93            rules: GraphNodeOpts::default(),
94            branches: BTreeMap::new(),
95        }
96    }
97}
98
99#[derive(Clone)]
100/// `Stratified` data container.
101pub struct Stratified<T, R> {
102    /// `rules` field for rules.
103    pub rules: Node<Vec<StratifyRule<R>>>,
104    /// `branches` field for branches.
105    pub branches: BTreeMap<String, Node<T>>,
106}
107
108/// Creates or computes `stratify_branch`.
109pub fn stratify_branch<T, R, F>(
110    graph: &Graph,
111    source: &Node<T>,
112    rules: &Node<R>,
113    classifier: F,
114    opts: GraphNodeOpts,
115) -> Node<T>
116where
117    T: Clone + 'static,
118    R: Clone + 'static,
119    F: Fn(&R, &T) -> bool + 'static,
120{
121    let op = stratify_branch_operator(classifier, 0, 1);
122    graph.init_node(op, vec![source.erased(), rules.erased()], opts)
123}
124
125/// Creates or computes `stratify`.
126pub fn stratify<T, R, F>(
127    graph: &Graph,
128    source: &Node<T>,
129    rules: Vec<StratifyRule<R>>,
130    classifier: F,
131    opts: StratifyOptions,
132) -> Stratified<T, R>
133where
134    T: Clone + 'static,
135    R: Clone + 'static,
136    F: Fn(&R, &T) -> bool + 'static,
137{
138    let mut seen = BTreeSet::new();
139    for rule in &rules {
140        assert!(
141            seen.insert(rule.name.clone()),
142            "stratify: duplicate rule name '{}'",
143            rule.name
144        );
145    }
146
147    let mut rules_opts = opts.rules.clone();
148    if rules_opts.name.is_none() {
149        rules_opts.name = Some(format!("{}/rules", opts.prefix));
150    }
151    rules_opts
152        .meta
153        .entry("kind".to_owned())
154        .or_insert_with(|| "stratify_rules".to_owned());
155    let rules_node = graph.state_opts(rules.clone(), rules_opts);
156
157    let classifier = Rc::new(classifier);
158    let mut branches = BTreeMap::new();
159    for rule in rules {
160        let branch_name = rule.name.clone();
161        let classifier = classifier.clone();
162        let op = stratify_branch_operator(
163            move |all: &Vec<StratifyRule<R>>, value: &T| {
164                all.iter()
165                    .find(|candidate| candidate.name == branch_name)
166                    .map(|current| classifier(&current.rule, value))
167                    .unwrap_or(false)
168            },
169            0,
170            1,
171        );
172        let mut branch_opts = opts.branches.get(&rule.name).cloned().unwrap_or_default();
173        if branch_opts.name.is_none() {
174            branch_opts.name = Some(format!("{}/{}", opts.prefix, rule.name));
175        }
176        for (key, value) in rule.meta {
177            branch_opts.meta.entry(key).or_insert(value);
178        }
179        branch_opts
180            .meta
181            .entry("branch".to_owned())
182            .or_insert_with(|| rule.name.clone());
183        let branch = graph.init_node(op, vec![source.erased(), rules_node.erased()], branch_opts);
184        branches.insert(rule.name, branch);
185    }
186
187    Stratified {
188        rules: rules_node,
189        branches,
190    }
191}
192
193fn stratify_branch_operator<T, R, F>(
194    classifier: F,
195    source_index: usize,
196    rules_index: usize,
197) -> Operator<T>
198where
199    T: Clone + 'static,
200    R: Clone + 'static,
201    F: Fn(&R, &T) -> bool + 'static,
202{
203    Operator::with_opts(
204        "stratifyBranch",
205        NodeOpts {
206            partial: true,
207            complete_when_deps_complete: false,
208            error_when_deps_error: false,
209            terminal_as_real_input: true,
210            ..NodeOpts::default()
211        },
212        move |ctx: &Ctx| {
213            if let Some(rules) = ctx.data::<R>(rules_index) {
214                for value in ctx.batch::<T>(source_index) {
215                    if classifier(rules.as_ref(), value.as_ref()) {
216                        ctx.emit((*value).clone());
217                    }
218                }
219            }
220
221            match ctx.terminal(source_index) {
222                Some(DepTerminal::Complete) => ctx.down(vec![Message::Complete]),
223                Some(DepTerminal::Error(error)) => {
224                    ctx.down(vec![Message::Error(error.to_string().into())]);
225                }
226                None => {}
227            }
228        },
229    )
230}