Skip to content

Commit 77a0508

Browse files
authored
Merge pull request #797 from elbeno/graphviz-refactor
✨ Add mermaid graph vizualization
2 parents 8051143 + 805c431 commit 77a0508

File tree

8 files changed

+273
-119
lines changed

8 files changed

+273
-119
lines changed

CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,11 +248,11 @@ target_sources(
248248
include/flow/flow.hpp
249249
include/flow/func_list.hpp
250250
include/flow/graph_builder.hpp
251-
include/flow/graphviz_builder.hpp
252251
include/flow/log.hpp
253252
include/flow/run.hpp
254253
include/flow/service.hpp
255-
include/flow/step.hpp)
254+
include/flow/step.hpp
255+
include/flow/viz_builder.hpp)
256256

257257
add_library(cib_seq INTERFACE)
258258
target_compile_features(cib_seq INTERFACE cxx_std_20)

docs/flows.adoc

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -342,13 +342,10 @@ what happens by default when we run the flow.
342342
----
343343
// the default flow builder and service
344344
template <stdx::ct_string Name = "">
345-
using builder = flow::graph<Name, flow::graph_builder<impl>>;
345+
using builder = flow::builder_for<flow::graph_builder<Name>>;
346346
347347
template <stdx::ct_string Name = "">
348-
struct service {
349-
using builder_t = builder<Name>;
350-
using interface_t = flow::FunctionPtr;
351-
};
348+
using service = flow::service_for<builder<Name>>;
352349
353350
// declare a flow service
354351
struct MorningRoutine : public service<"MorningRoutine"> {};
@@ -358,29 +355,22 @@ nexus.service<"MorningRoutine">();
358355
----
359356

360357
Here `graph_builder` is the type that renders the flow description into the
361-
array of function pointers, and `flow::FunctionPtr` is the type-erased interface
362-
(here a function taking no arguments and returning `void`) that is called to run
363-
a flow.
358+
array of function pointers.
364359

365360
But given a flow, other renderings are possible.
366361

367362
[source,cpp]
368363
----
369364
// a flow builder and service that produces a graphviz rendering
370365
template <stdx::ct_string Name = "">
371-
using viz_builder = flow::graph<Name, flow::graphviz_builder>;
366+
using viz_builder = flow::graph<Name, flow::viz_builder>;
372367
373368
template <stdx::ct_string Name = "">
374-
struct viz_service {
375-
using builder_t = builder<Name>;
376-
using interface_t = flow::VizFunctionPtr;
377-
};
369+
using viz_service = flow::service_for<viz_builder>;
378370
----
379371

380372
Here, `viz_service` will produce a graphviz rendering of a flow using the
381-
`graphviz_builder`. `flow::VizFunctionPtr` is the type-erased interface once
382-
more, and it is defined to take no arguments and return a `std::string`. When we "run"
383-
the flow, we get the graphviz rendering.
373+
`viz_builder`. When we "run" the flow, we get the graphviz rendering.
384374

385375
[source,cpp]
386376
----
@@ -392,6 +382,6 @@ struct MorningRoutine : public viz_service<"MorningRoutine"> {};
392382
auto graphviz_str = nexus.service<"MorningRoutine">();
393383
----
394384

395-
`graphviz_builder` is available as a debugging aid. But in general, having the
385+
`viz_builder` is available as a debugging aid. But in general, having the
396386
flow rendering separate from the flow definition enables any kind of rendering
397-
with correponding runtime behaviour.
387+
with corresponding runtime behaviour.

include/flow/graphviz_builder.hpp

Lines changed: 0 additions & 79 deletions
This file was deleted.

include/flow/step.hpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,16 @@ struct ct_node {
1818
using is_subgraph = void;
1919
using name_t = stdx::cts_t<Name>;
2020
using func_t = F;
21+
using cond_t = Cond;
2122

2223
constexpr static auto ct_type = Type;
2324
constexpr static auto ct_name = Name;
2425
constexpr static auto identity = Identity;
25-
constexpr static auto condition = Cond{};
26+
constexpr static auto condition = cond_t{};
2627

2728
constexpr auto operator*() const {
2829
if constexpr (Identity == dsl::subgraph_identity::REFERENCE) {
29-
return ct_node<Type, Name, dsl::subgraph_identity::VALUE, Cond,
30+
return ct_node<Type, Name, dsl::subgraph_identity::VALUE, cond_t,
3031
F>{};
3132
} else {
3233
return ct_node{};
@@ -42,6 +43,9 @@ template <stdx::ct_string Type, stdx::ct_string Name, typename F>
4243
}
4344

4445
constexpr auto empty_func = []() -> void {};
46+
47+
template <typename Node>
48+
constexpr bool is_milestone = Node::ct_type == stdx::ct_string{"milestone"};
4549
} // namespace detail
4650

4751
template <stdx::ct_string Name, typename F>

include/flow/viz_builder.hpp

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
#pragma once
2+
3+
#include <flow/dsl/walk.hpp>
4+
#include <flow/step.hpp>
5+
6+
#include <stdx/ct_string.hpp>
7+
#include <stdx/tuple.hpp>
8+
#include <stdx/tuple_algorithms.hpp>
9+
#include <stdx/utility.hpp>
10+
11+
#include <algorithm>
12+
#include <array>
13+
#include <iterator>
14+
#include <string>
15+
#include <string_view>
16+
#include <type_traits>
17+
#include <utility>
18+
19+
namespace flow {
20+
struct graphviz {
21+
template <typename Graph> static auto prologue() -> std::string {
22+
std::string output{"digraph "};
23+
output += std::string_view{Graph::name};
24+
output += " {";
25+
return output;
26+
}
27+
template <typename Graph> static auto epilogue() -> std::string {
28+
return "}";
29+
}
30+
template <typename Node> static auto render_node(Node) -> std::string {
31+
std::string output{" "};
32+
output += std::string_view{Node::name_t::value};
33+
return output;
34+
}
35+
template <typename Edge> static auto render_edge(Edge) -> std::string {
36+
std::string output{" "};
37+
output += std::string_view{Edge::source_t::name_t::value};
38+
output += " -> ";
39+
output += std::string_view{Edge::dest_t::name_t::value};
40+
return output;
41+
}
42+
};
43+
44+
class mermaid {
45+
static auto to_id(auto str) {
46+
std::string s{"_"};
47+
s += str;
48+
std::replace(std::begin(s), std::end(s), ' ', '_');
49+
return s;
50+
}
51+
52+
public:
53+
template <typename Graph> static auto prologue() -> std::string {
54+
std::string output{"---\ntitle: "};
55+
output += std::string_view{Graph::name};
56+
output += "\n---\nflowchart TD";
57+
return output;
58+
}
59+
60+
template <typename Graph> static auto epilogue() -> std::string {
61+
return "";
62+
}
63+
64+
template <typename Node> static auto render_node(Node) -> std::string {
65+
using namespace std::string_view_literals;
66+
std::string output{" "};
67+
constexpr auto node_opener = std::array{"("sv, "["sv};
68+
constexpr auto node_closer = std::array{")"sv, "]"sv};
69+
output += to_id(std::string_view{Node::name_t::value});
70+
output += node_opener[detail::is_milestone<Node>];
71+
output += std::string_view{Node::name_t::value};
72+
output += node_closer[detail::is_milestone<Node>];
73+
return output;
74+
}
75+
76+
template <typename Node>
77+
requires(Node::name_t::value == stdx::ct_string{"start"} or
78+
Node::name_t::value == stdx::ct_string{"end"})
79+
static auto render_node(Node) -> std::string {
80+
using namespace std::string_view_literals;
81+
std::string output{" "};
82+
output += to_id(std::string_view{Node::name_t::value});
83+
output += "((";
84+
output += std::string_view{Node::name_t::value};
85+
output += "))";
86+
return output;
87+
}
88+
89+
template <typename Edge> static auto render_edge(Edge) -> std::string {
90+
std::string output{" "};
91+
output += to_id(std::string_view{Edge::source_t::name_t::value});
92+
output += " --";
93+
if constexpr (Edge::cond_t::ct_name != stdx::ct_string{"always"}) {
94+
output += std::string_view{Edge::cond_t::ct_name};
95+
output += "--";
96+
}
97+
output += "> ";
98+
output += to_id(std::string_view{Edge::dest_t::name_t::value});
99+
return output;
100+
}
101+
};
102+
103+
namespace detail {
104+
template <typename N> struct matching_source {
105+
template <typename Edge>
106+
using fn = std::is_same<N, typename Edge::source_t>;
107+
};
108+
template <typename N> struct matching_dest {
109+
template <typename Edge> using fn = std::is_same<N, typename Edge::dest_t>;
110+
};
111+
112+
template <template <typename> typename Matcher, typename Edges>
113+
struct has_no_edge_q {
114+
template <typename Node>
115+
using fn =
116+
boost::mp11::mp_empty<boost::mp11::mp_copy_if_q<Edges, Matcher<Node>>>;
117+
};
118+
} // namespace detail
119+
120+
template <stdx::ct_string Name, typename Renderer = graphviz>
121+
struct viz_builder {
122+
template <typename Graph>
123+
[[nodiscard]] static auto build(Graph const &input) {
124+
using flow::operator""_milestone;
125+
126+
auto const nodes = stdx::to_sorted_set(flow::dsl::get_nodes(input));
127+
auto const edges = stdx::to_sorted_set(flow::dsl::get_edges(input));
128+
using nodes_t = std::remove_cvref_t<decltype(nodes)>;
129+
using edges_t = std::remove_cvref_t<decltype(edges)>;
130+
131+
auto const sources = stdx::filter<
132+
detail::has_no_edge_q<detail::matching_dest, edges_t>::template fn>(
133+
nodes);
134+
auto const sinks =
135+
stdx::filter<detail::has_no_edge_q<detail::matching_source,
136+
edges_t>::template fn>(nodes);
137+
138+
constexpr auto start = "start"_milestone;
139+
constexpr auto end = "end"_milestone;
140+
using start_t = std::remove_cvref_t<decltype(start)>;
141+
using end_t = std::remove_cvref_t<decltype(end)>;
142+
143+
constexpr auto edge_from_start = []<typename N>(N) {
144+
return dsl::edge<start_t, N, typename N::cond_t>{};
145+
};
146+
constexpr auto edge_to_end = []<typename N>(N) {
147+
return dsl::edge<N, end_t, typename N::cond_t>{};
148+
};
149+
150+
auto const renderable_nodes =
151+
stdx::tuple_cat(stdx::tuple{start, end}, nodes);
152+
auto const renderable_edges =
153+
stdx::tuple_cat(edges, stdx::transform(edge_from_start, sources),
154+
stdx::transform(edge_to_end, sinks));
155+
156+
auto prologue = Renderer::template prologue<Graph>();
157+
auto epilogue = Renderer::template epilogue<Graph>();
158+
auto rendered_nodes = stdx::transform(
159+
[](auto n) { return Renderer::render_node(n); }, renderable_nodes);
160+
auto rendered_edges = stdx::transform(
161+
[](auto e) { return Renderer::render_edge(e); }, renderable_edges);
162+
163+
return prologue + '\n' +
164+
std::move(rendered_nodes)
165+
.join(std::string{},
166+
[](auto &&x, auto &&y) {
167+
return FWD(x) + '\n' + FWD(y);
168+
}) +
169+
'\n' +
170+
std::move(rendered_edges)
171+
.join(std::string{},
172+
[](auto &&x, auto &&y) {
173+
return FWD(x) + '\n' + FWD(y);
174+
}) +
175+
'\n' + epilogue;
176+
}
177+
178+
constexpr static auto name = Name;
179+
using interface_t = auto (*)() -> std::string;
180+
181+
template <typename Initialized> class built_flow {
182+
static auto built() {
183+
auto const v = Initialized::value;
184+
return build(v);
185+
}
186+
187+
static auto run() -> std::string { return built(); }
188+
189+
public:
190+
// NOLINTNEXTLINE(google-explicit-constructor)
191+
constexpr explicit(false) operator interface_t() const { return run; }
192+
auto operator()() const { return run(); }
193+
constexpr static bool active = true;
194+
};
195+
196+
template <typename Initialized>
197+
[[nodiscard]] constexpr static auto render() -> built_flow<Initialized> {
198+
return {};
199+
}
200+
};
201+
} // namespace flow

0 commit comments

Comments
 (0)