Dot file output
TODO put everything in a library
This commit is contained in:
@ -1,9 +1,7 @@
|
|||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use petgraph::dot::Dot;
|
|
||||||
|
|
||||||
use crate::rsprocess::structure::{RSset, RSsystem};
|
use crate::rsprocess::structure::{RSset, RSsystem};
|
||||||
use crate::rsprocess::{graph, translator};
|
use crate::rsprocess::{graph, rsdot, translator};
|
||||||
use crate::rsprocess::translator::Translator;
|
use crate::rsprocess::translator::Translator;
|
||||||
use crate::rsprocess::{frequency, perpetual, statistics, transitions};
|
use crate::rsprocess::{frequency, perpetual, statistics, transitions};
|
||||||
|
|
||||||
@ -251,10 +249,11 @@ pub fn digraph() -> std::io::Result<()> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("Generated graph in dot notation:\n{:?}\n", Dot::new(&res));
|
|
||||||
|
|
||||||
let rc_translator = Rc::new(translator);
|
let rc_translator = Rc::new(translator);
|
||||||
|
|
||||||
|
let old_res = Rc::new(res.clone());
|
||||||
|
|
||||||
|
// map each value to the corresponding value we want to display
|
||||||
let res = res.map(
|
let res = res.map(
|
||||||
|id, node|
|
|id, node|
|
||||||
graph::GraphMapNodesTy::from(graph::GraphMapNodes::Entities,
|
graph::GraphMapNodesTy::from(graph::GraphMapNodes::Entities,
|
||||||
@ -263,9 +262,17 @@ pub fn digraph() -> std::io::Result<()> {
|
|||||||
&graph::GraphMapNodesTy::from(graph::GraphMapNodes::Context,
|
&graph::GraphMapNodesTy::from(graph::GraphMapNodes::Context,
|
||||||
Rc::clone(&rc_translator)).get()(id, node),
|
Rc::clone(&rc_translator)).get()(id, node),
|
||||||
graph::GraphMapEdgesTy::from(graph::GraphMapEdges::EntitiesAdded,
|
graph::GraphMapEdgesTy::from(graph::GraphMapEdges::EntitiesAdded,
|
||||||
Rc::clone(&rc_translator)).get());
|
Rc::clone(&rc_translator)).get()
|
||||||
|
);
|
||||||
|
|
||||||
println!("Generated graph in dot notation:\n{}", Dot::new(&res));
|
println!("Generated graph in dot notation:\n{}",
|
||||||
|
rsdot::RSDot::with_attr_getters(
|
||||||
|
&res,
|
||||||
|
&[],
|
||||||
|
&graph::default_edge_formatter(Rc::clone(&old_res)),
|
||||||
|
&graph::default_node_formatter(Rc::clone(&old_res)),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,18 @@
|
|||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use petgraph::Graph;
|
use petgraph::{Graph, Directed};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use super::structure::{RSlabel, RSsystem, RSset};
|
use super::structure::{RSlabel, RSsystem, RSset, RSprocess};
|
||||||
use super::support_structures::TransitionsIterator;
|
use super::support_structures::TransitionsIterator;
|
||||||
use super::translator;
|
use super::translator;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
type RSgraph = Graph<RSsystem, RSlabel, Directed, u32>;
|
||||||
|
|
||||||
pub fn digraph(
|
pub fn digraph(
|
||||||
system: RSsystem
|
system: RSsystem
|
||||||
) -> Result<Graph<RSsystem, RSlabel>, String> {
|
) -> Result<RSgraph, String> {
|
||||||
let mut graph: Graph<RSsystem, RSlabel> = Graph::new();
|
let mut graph = Graph::default();
|
||||||
let node = graph.add_node(system.clone());
|
let node = graph.add_node(system.clone());
|
||||||
|
|
||||||
let mut association = HashMap::new();
|
let mut association = HashMap::new();
|
||||||
@ -351,3 +353,56 @@ impl GraphMapEdgesTy {
|
|||||||
&self.function
|
&self.function
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Formatting Nodes & Edges
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
use petgraph::visit::{IntoNodeReferences, IntoEdgeReferences, EdgeRef};
|
||||||
|
|
||||||
|
type RSdotGraph = Graph<String, String, Directed, u32>;
|
||||||
|
type RSformatNodeTy =
|
||||||
|
dyn Fn(&RSdotGraph, <&RSdotGraph as IntoNodeReferences>::NodeRef) -> String;
|
||||||
|
|
||||||
|
type RSformatEdgeTy =
|
||||||
|
dyn Fn(&RSdotGraph, <&RSdotGraph as IntoEdgeReferences>::EdgeRef) -> String;
|
||||||
|
|
||||||
|
pub fn default_node_formatter(
|
||||||
|
original_graph: Rc<RSgraph>
|
||||||
|
) -> Box<RSformatNodeTy>
|
||||||
|
{
|
||||||
|
Box::new(
|
||||||
|
move |_g, n|
|
||||||
|
String::from(
|
||||||
|
match original_graph.node_weight(n.0).unwrap().get_context_process()
|
||||||
|
{
|
||||||
|
RSprocess::Nill =>
|
||||||
|
", fillcolor=white",
|
||||||
|
RSprocess::RecursiveIdentifier { identifier: _ } =>
|
||||||
|
", fillcolor=\"#BBFF99\"",
|
||||||
|
RSprocess::EntitySet { entities: _, next_process: _ } =>
|
||||||
|
", fillcolor=\"#AAEEFF\"",
|
||||||
|
RSprocess::NondeterministicChoice { children: _ } =>
|
||||||
|
", fillcolor=\"#FFEE99\"",
|
||||||
|
RSprocess::Summation { children: _ } =>
|
||||||
|
", fillcolor=\"#CC99FF\"",
|
||||||
|
RSprocess::WaitEntity
|
||||||
|
{ repeat: _, repeated_process: _, next_process: _ } =>
|
||||||
|
", fillcolor=\"#FF99AA\"",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_edge_formatter(
|
||||||
|
original_graph: Rc<RSgraph>
|
||||||
|
) -> Box<RSformatEdgeTy>
|
||||||
|
{
|
||||||
|
Box::new(
|
||||||
|
move |_g, e| String::from(
|
||||||
|
if original_graph.edge_weight(e.id()).unwrap().products.is_empty() {
|
||||||
|
"color=black, fontcolor=black"
|
||||||
|
} else {
|
||||||
|
"color=blue, fontcolor=blue"
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|||||||
@ -8,3 +8,4 @@ pub mod support_structures;
|
|||||||
pub mod transitions;
|
pub mod transitions;
|
||||||
pub mod translator;
|
pub mod translator;
|
||||||
pub mod graph;
|
pub mod graph;
|
||||||
|
pub mod rsdot;
|
||||||
|
|||||||
341
src/rsprocess/rsdot.rs
Normal file
341
src/rsprocess/rsdot.rs
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
//! Slightly modified Simple graphviz dot file format output.
|
||||||
|
//! See petgraph::dot::mod.
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
// use alloc::string::String;
|
||||||
|
use core::fmt::{self, Display, Write};
|
||||||
|
use petgraph::
|
||||||
|
{
|
||||||
|
data::DataMap,
|
||||||
|
visit::{
|
||||||
|
EdgeRef,
|
||||||
|
GraphProp,
|
||||||
|
IntoEdgeReferences,
|
||||||
|
IntoNodeReferences,
|
||||||
|
NodeIndexable,
|
||||||
|
NodeRef,
|
||||||
|
}};
|
||||||
|
|
||||||
|
pub struct RSDot<'a, G>
|
||||||
|
where
|
||||||
|
G: IntoEdgeReferences + IntoNodeReferences + DataMap,
|
||||||
|
{
|
||||||
|
graph: G,
|
||||||
|
get_edge_attributes: &'a dyn Fn(G, G::EdgeRef) -> String,
|
||||||
|
get_node_attributes: &'a dyn Fn(G, G::NodeRef) -> String,
|
||||||
|
config: Configs,
|
||||||
|
}
|
||||||
|
|
||||||
|
static TYPE: [&str; 2] = ["graph", "digraph"];
|
||||||
|
static EDGE: [&str; 2] = ["--", "->"];
|
||||||
|
static INDENT: &str = " ";
|
||||||
|
|
||||||
|
impl<'a, G> RSDot<'a, G>
|
||||||
|
where
|
||||||
|
G: IntoNodeReferences + IntoEdgeReferences + DataMap,
|
||||||
|
{
|
||||||
|
/// Create a `Dot` formatting wrapper with default configuration.
|
||||||
|
#[inline]
|
||||||
|
pub fn new(graph: G) -> Self {
|
||||||
|
RSDot {
|
||||||
|
graph,
|
||||||
|
get_edge_attributes: &|_, _| String::new(),
|
||||||
|
get_node_attributes: &|_, _| String::new(),
|
||||||
|
config: Configs::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a `Dot` formatting wrapper with custom configuration.
|
||||||
|
#[inline]
|
||||||
|
pub fn with_config(graph: G, config: &'a [Config]) -> Self {
|
||||||
|
let config = Configs::extract(config);
|
||||||
|
RSDot {
|
||||||
|
graph,
|
||||||
|
get_edge_attributes: &|_, _| String::new(),
|
||||||
|
get_node_attributes: &|_, _| String::new(),
|
||||||
|
config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn with_attr_getters(
|
||||||
|
graph: G,
|
||||||
|
config: &'a [Config],
|
||||||
|
get_edge_attributes: &'a dyn Fn(G, G::EdgeRef) -> String,
|
||||||
|
get_node_attributes: &'a dyn Fn(G, G::NodeRef) -> String,
|
||||||
|
) -> Self {
|
||||||
|
let config = Configs::extract(config);
|
||||||
|
RSDot {
|
||||||
|
graph,
|
||||||
|
get_edge_attributes,
|
||||||
|
get_node_attributes,
|
||||||
|
config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Direction of graph layout.
|
||||||
|
///
|
||||||
|
/// <https://graphviz.org/docs/attrs/rankdir/>
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum RankDir {
|
||||||
|
/// Top to bottom
|
||||||
|
#[default]
|
||||||
|
TB,
|
||||||
|
/// Bottom to top
|
||||||
|
BT,
|
||||||
|
/// Left to right
|
||||||
|
LR,
|
||||||
|
/// Right to left
|
||||||
|
RL,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct NodeStyle {
|
||||||
|
shape: String,
|
||||||
|
style: String
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for NodeStyle {
|
||||||
|
fn default() -> Self {
|
||||||
|
NodeStyle { shape: "box".to_string(),
|
||||||
|
style: "filled, rounded".to_string() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct EdgeStyle {
|
||||||
|
arrowhead: String
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EdgeStyle {
|
||||||
|
fn default() -> Self {
|
||||||
|
EdgeStyle { arrowhead: "vee".to_string() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Dot` configuration.
|
||||||
|
///
|
||||||
|
/// This enum does not have an exhaustive definition (will be expanded)
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub enum Config {
|
||||||
|
/// Sets direction of graph layout.
|
||||||
|
RankDir(RankDir),
|
||||||
|
/// Node style
|
||||||
|
NodeStyle(NodeStyle),
|
||||||
|
/// Edge style
|
||||||
|
EdgeStyle(EdgeStyle),
|
||||||
|
}
|
||||||
|
macro_rules! make_config_struct {
|
||||||
|
($($variant:ident,)*) => {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
struct Configs {
|
||||||
|
$($variant: bool,)*
|
||||||
|
RankDir: Option<RankDir>,
|
||||||
|
NodeStyle: Option<NodeStyle>,
|
||||||
|
EdgeStyle: Option<EdgeStyle>,
|
||||||
|
}
|
||||||
|
impl Configs {
|
||||||
|
#[inline]
|
||||||
|
fn extract(configs: &[Config]) -> Self {
|
||||||
|
let mut conf = Self::default();
|
||||||
|
for c in configs {
|
||||||
|
match c {
|
||||||
|
$(Config::$variant => conf.$variant = true,)*
|
||||||
|
Config::RankDir(dir) => conf.RankDir = Some(*dir),
|
||||||
|
Config::NodeStyle(style) =>
|
||||||
|
conf.NodeStyle = Some(style.clone()),
|
||||||
|
Config::EdgeStyle(style) =>
|
||||||
|
conf.EdgeStyle = Some(style.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Default for Configs {
|
||||||
|
fn default() -> Self {
|
||||||
|
Configs {
|
||||||
|
$(Config::$variant: true,)*
|
||||||
|
RankDir: Some(RankDir::default()),
|
||||||
|
NodeStyle: Some(NodeStyle::default()),
|
||||||
|
EdgeStyle: Some(EdgeStyle::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
make_config_struct!();
|
||||||
|
|
||||||
|
impl<G> RSDot<'_, G>
|
||||||
|
where
|
||||||
|
G: IntoNodeReferences + IntoEdgeReferences + NodeIndexable + GraphProp + DataMap,
|
||||||
|
{
|
||||||
|
fn graph_fmt<NF, EF>(
|
||||||
|
&self,
|
||||||
|
f: &mut fmt::Formatter,
|
||||||
|
node_fmt: NF,
|
||||||
|
edge_fmt: EF
|
||||||
|
) -> fmt::Result
|
||||||
|
where
|
||||||
|
NF: Fn(&G::NodeWeight, &mut fmt::Formatter) -> fmt::Result,
|
||||||
|
EF: Fn(&G::EdgeWeight, &mut fmt::Formatter) -> fmt::Result,
|
||||||
|
{
|
||||||
|
let g = self.graph;
|
||||||
|
writeln!(f, "{} {{", TYPE[g.is_directed() as usize])?;
|
||||||
|
|
||||||
|
if let Some(rank_dir) = &self.config.RankDir {
|
||||||
|
let value = match rank_dir {
|
||||||
|
RankDir::TB => "TB",
|
||||||
|
RankDir::BT => "BT",
|
||||||
|
RankDir::LR => "LR",
|
||||||
|
RankDir::RL => "RL",
|
||||||
|
};
|
||||||
|
writeln!(f, "{INDENT}rankdir=\"{value}\"\n")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(style) = &self.config.NodeStyle {
|
||||||
|
writeln!(f,
|
||||||
|
"{INDENT}node [shape=\"{}\", style=\"{}\"]",
|
||||||
|
style.shape,
|
||||||
|
style.style)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(style) = &self.config.EdgeStyle {
|
||||||
|
writeln!(f, "{INDENT}edge [arrowhead=\"{}\"]\n", style.arrowhead)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// output all labels
|
||||||
|
for node in g.node_references() {
|
||||||
|
write!(f, "{INDENT}\"")?;
|
||||||
|
// Escaped(FnFmt(node.weight(), &node_fmt)).fmt(f)?;
|
||||||
|
write!(f, "{}", g.to_index(node.id()))?;
|
||||||
|
write!(f, "\" [ ")?;
|
||||||
|
|
||||||
|
write!(f, "label = \"")?;
|
||||||
|
Escaped(FnFmt(node.weight(), &node_fmt)).fmt(f)?;
|
||||||
|
write!(f, "\" ")?;
|
||||||
|
|
||||||
|
writeln!(f, "{}]", (self.get_node_attributes)(g, node))?;
|
||||||
|
}
|
||||||
|
// output all edges
|
||||||
|
for edge in g.edge_references() {
|
||||||
|
write!(f, "{INDENT}\"")?;
|
||||||
|
// let node_source_weight = g.node_weight(edge.source()).unwrap();
|
||||||
|
// Escaped(FnFmt(node_source_weight, &node_fmt)).fmt(f)?;
|
||||||
|
write!(f, "{}", g.to_index(edge.source()))?;
|
||||||
|
write!(f, "\" {} \"", EDGE[g.is_directed() as usize])?;
|
||||||
|
// let node_target_weight = g.node_weight(edge.target()).unwrap();
|
||||||
|
// Escaped(FnFmt(node_target_weight, &node_fmt)).fmt(f)?;
|
||||||
|
write!(f, "{}", g.to_index(edge.target()))?;
|
||||||
|
write!(f, "\" [ ")?;
|
||||||
|
|
||||||
|
write!(f, "label = \"")?;
|
||||||
|
Escaped(FnFmt(edge.weight(), &edge_fmt)).fmt(f)?;
|
||||||
|
write!(f, "\" ")?;
|
||||||
|
|
||||||
|
writeln!(f, "{}]", (self.get_edge_attributes)(g, edge))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(f, "}}")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<G> fmt::Display for RSDot<'_, G>
|
||||||
|
where
|
||||||
|
G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp + DataMap,
|
||||||
|
G::EdgeWeight: fmt::Display,
|
||||||
|
G::NodeWeight: fmt::Display,
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
self.graph_fmt(f, fmt::Display::fmt, fmt::Display::fmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<G> fmt::LowerHex for RSDot<'_, G>
|
||||||
|
where
|
||||||
|
G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp + DataMap,
|
||||||
|
G::EdgeWeight: fmt::LowerHex,
|
||||||
|
G::NodeWeight: fmt::LowerHex,
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
self.graph_fmt(f, fmt::LowerHex::fmt, fmt::LowerHex::fmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<G> fmt::UpperHex for RSDot<'_, G>
|
||||||
|
where
|
||||||
|
G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp + DataMap,
|
||||||
|
G::EdgeWeight: fmt::UpperHex,
|
||||||
|
G::NodeWeight: fmt::UpperHex,
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
self.graph_fmt(f, fmt::UpperHex::fmt, fmt::UpperHex::fmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<G> fmt::Debug for RSDot<'_, G>
|
||||||
|
where
|
||||||
|
G: IntoEdgeReferences + IntoNodeReferences + NodeIndexable + GraphProp + DataMap,
|
||||||
|
G::EdgeWeight: fmt::Debug,
|
||||||
|
G::NodeWeight: fmt::Debug,
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
self.graph_fmt(f, fmt::Debug::fmt, fmt::Debug::fmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Escape for Graphviz
|
||||||
|
struct Escaper<W>(W);
|
||||||
|
|
||||||
|
impl<W> fmt::Write for Escaper<W>
|
||||||
|
where
|
||||||
|
W: fmt::Write,
|
||||||
|
{
|
||||||
|
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||||
|
for c in s.chars() {
|
||||||
|
self.write_char(c)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_char(&mut self, c: char) -> fmt::Result {
|
||||||
|
match c {
|
||||||
|
'"' | '\\' => self.0.write_char('\\')?,
|
||||||
|
// \l is for left justified linebreak
|
||||||
|
'\n' => return self.0.write_str("\\l"),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
self.0.write_char(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pass Display formatting through a simple escaping filter
|
||||||
|
struct Escaped<T>(T);
|
||||||
|
|
||||||
|
impl<T> fmt::Display for Escaped<T>
|
||||||
|
where
|
||||||
|
T: fmt::Display,
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
if f.alternate() {
|
||||||
|
writeln!(&mut Escaper(f), "{:#}", &self.0)
|
||||||
|
} else {
|
||||||
|
write!(&mut Escaper(f), "{}", &self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format data using a specific format function
|
||||||
|
struct FnFmt<'a, T, F>(&'a T, F);
|
||||||
|
|
||||||
|
impl<'a, T, F> fmt::Display for FnFmt<'a, T, F>
|
||||||
|
where
|
||||||
|
F: Fn(&'a T, &mut fmt::Formatter<'_>) -> fmt::Result,
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
self.1(self.0, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -108,6 +108,10 @@ impl RSset {
|
|||||||
pub fn push(&mut self, b: &RSset) {
|
pub fn push(&mut self, b: &RSset) {
|
||||||
self.identifiers.extend(b.iter())
|
self.identifiers.extend(b.iter())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.identifiers.is_empty()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for RSset {
|
impl Default for RSset {
|
||||||
|
|||||||
Reference in New Issue
Block a user