///|
/// Explicit source binding; positions use the library UTF-16 span contract.
pub struct SourceBinding {
format : String
path : String
span : @diagrams.Span?
} derive(Eq, Debug)
///|
pub fn SourceBinding::new(
format : String,
path : String,
span? : @diagrams.Span,
) -> SourceBinding {
{ format, path, span, }
}
///|
pub(all) enum Role {
Generic
Action
Decision
Merge
Fork
Join
Start
ActivityFinal
FlowFinal
Connector
} derive(Eq, Debug)
///|
pub(all) enum ConnectionRole {
Flow
Branch
LoopBack
LoopExit
Parallel
NoteAttachment
Layout
} derive(Eq, Debug)
///|
pub(all) enum RegionKind {
Conditional
Loop
Parallel
Split
} derive(Eq, Debug)
///|
pub struct Branch {
id : String
label : String
members : ReadOnlyArray[String]
condition : String?
origin : SourceBinding?
} derive(Eq, Debug)
///|
pub fn Branch::new(
id : String,
label~ : String = "",
members~ : ArrayView[String] = [],
condition? : String,
origin? : SourceBinding,
) -> Branch {
{ id, label, members: ReadOnlyArray::from_array(members), condition, origin, }
}
///|
pub struct Region {
id : String
kind : RegionKind
parent : String?
entry : String?
exit : String?
members : ReadOnlyArray[String]
branches : ReadOnlyArray[Branch]
condition : String?
join_specification : String?
origin : SourceBinding?
} derive(Eq, Debug)
///|
pub fn Region::new(
id : String,
kind : RegionKind,
parent? : String,
entry? : String,
exit? : String,
members~ : ArrayView[String] = [],
branches~ : ArrayView[Branch] = [],
condition? : String,
join_specification? : String,
origin? : SourceBinding,
) -> Region {
{
id,
kind,
parent,
entry,
exit,
members: ReadOnlyArray::from_array(members),
branches: ReadOnlyArray::from_array(branches),
condition,
join_specification,
origin,
}
}
///|
pub struct Note {
id : String
text : String
targets : ReadOnlyArray[String]
connection_targets : ReadOnlyArray[String]
placement : String
groups : ReadOnlyArray[String]
lane : String?
origin : SourceBinding?
} derive(Eq, Debug)
///|
pub fn Note::new(
id : String,
text : String,
targets~ : ArrayView[String] = [],
connection_targets~ : ArrayView[String] = [],
placement~ : String = "right",
groups~ : ArrayView[String] = [],
lane? : String,
origin? : SourceBinding,
) -> Note {
{
id,
text,
targets: ReadOnlyArray::from_array(targets),
connection_targets: ReadOnlyArray::from_array(connection_targets),
placement,
groups: ReadOnlyArray::from_array(groups),
lane,
origin,
}
}
///|
pub(all) enum Direction {
TB
BT
LR
RL
} derive(Eq, Debug)
///|
pub(all) enum LabelKind {
Text
Markdown
Html
} derive(Eq, Debug)
///|
pub(all) enum LineStyle {
Solid
Dotted
Thick
Invisible
} derive(Eq, Debug)
///|
pub(all) enum Marker {
None
Arrow
Circle
Cross
} derive(Eq, Debug)
///|
pub(all) enum FlowDirection {
Forward
Backward
Both
Undirected
Invisible
} derive(Eq, Debug)
///|
pub struct RenderProperties {
link : String?
metadata : ReadOnlyArray[(String, String)]
} derive(Eq, Debug)
///|
pub fn RenderProperties::new(
link? : String,
metadata~ : ArrayView[(String, String)] = [],
) -> RenderProperties {
{ link, metadata: ReadOnlyArray::from_array(metadata), }
}
///|
pub struct Lane {
id : String
label : String
direction : Direction?
expanded : Bool
} derive(Eq, Debug)
///|
pub fn Lane::new(
id : String,
label~ : String = id,
direction? : Direction,
expanded~ : Bool = true,
) -> Lane {
{ id, label, direction, expanded, }
}
///|
pub struct Group {
id : String
label : String
parent : String?
lane : String?
direction : Direction?
expanded : Bool
} derive(Eq, Debug)
///|
pub fn Group::new(
id : String,
label~ : String = id,
parent? : String,
lane? : String,
direction? : Direction,
expanded~ : Bool = true,
) -> Group {
{ id, label, parent, lane, direction, expanded, }
}
///|
pub struct Node {
id : String
label : String
label_kind : LabelKind
shape : String
metadata : ReadOnlyArray[(String, String)]
lane : String?
groups : ReadOnlyArray[String]
properties : RenderProperties?
role : Role
regions : ReadOnlyArray[String]
origin : SourceBinding?
} derive(Eq, Debug)
///|
pub fn Node::new(
id : String,
label~ : String = id,
label_kind~ : LabelKind = Text,
shape~ : String = "rect",
metadata~ : ArrayView[(String, String)] = [],
lane? : String,
groups~ : ArrayView[String] = [],
properties? : RenderProperties,
role~ : Role = Generic,
regions~ : ArrayView[String] = [],
origin? : SourceBinding,
) -> Node {
{
id,
label,
label_kind,
shape: canonical_shape(shape),
metadata: ReadOnlyArray::from_array(metadata),
lane,
groups: ReadOnlyArray::from_array(groups),
properties,
role,
regions: ReadOnlyArray::from_array(regions),
origin,
}
}
///|
pub struct Connection {
id : String
source : String
target : String
label : String
label_kind : LabelKind
style : LineStyle
source_marker : Marker
target_marker : Marker
min_length : Int32
direction : FlowDirection
direction_hint : Direction?
role : ConnectionRole
origin : SourceBinding?
} derive(Eq, Debug)
///|
pub fn Connection::new(
id : String,
source : String,
target : String,
label~ : String = "",
label_kind~ : LabelKind = Text,
style~ : LineStyle = Solid,
source_marker~ : Marker = None,
target_marker~ : Marker = Arrow,
min_length~ : Int32 = 1,
direction_hint? : Direction,
role~ : ConnectionRole = Flow,
origin? : SourceBinding,
) -> Connection {
let direction : FlowDirection = if style == Invisible {
Invisible
} else if source_marker == None && target_marker == None {
Undirected
} else if source_marker != None && target_marker != None {
Both
} else if source_marker != None {
Backward
} else {
Forward
}
{
id,
source,
target,
label,
label_kind,
style,
source_marker,
target_marker,
min_length,
direction,
direction_hint,
role,
origin,
}
}
///|
pub struct Flowchart {
notes : ReadOnlyArray[Note]
regions : ReadOnlyArray[Region]
id : String
direction : Direction
lanes : ReadOnlyArray[Lane]
groups : ReadOnlyArray[Group]
nodes : ReadOnlyArray[Node]
connections : ReadOnlyArray[Connection]
title : String?
accessibility_title : String?
accessibility_description : String?
} derive(Eq, Debug)
///|
pub fn Flowchart::new(
id : String,
nodes : ArrayView[Node],
connections : ArrayView[Connection],
direction~ : Direction = TB,
lanes~ : ArrayView[Lane] = [],
groups~ : ArrayView[Group] = [],
title? : String,
accessibility_title? : String,
accessibility_description? : String,
notes~ : ArrayView[Note] = [],
regions~ : ArrayView[Region] = [],
) -> Flowchart {
{
notes: ReadOnlyArray::from_array(notes),
regions: ReadOnlyArray::from_array(regions),
id,
direction,
lanes: ReadOnlyArray::from_array(lanes),
groups: ReadOnlyArray::from_array(groups),
nodes: ReadOnlyArray::from_array(nodes),
connections: ReadOnlyArray::from_array(connections),
title,
accessibility_title,
accessibility_description,
}
}
///|
pub fn direction(value : String) -> Direction? {
match value {
"TB" | "TD" => Some(TB)
"BT" => Some(BT)
"LR" => Some(LR)
"RL" => Some(RL)
_ => None
}
}
///|
pub fn canonical_shape(value : String) -> String {
match value {
"proc" | "process" | "square" | "rectangle" | "squareRect" => "rect"
"event" | "round" | "roundedRect" => "rounded"
"terminal" | "pill" => "stadium"
"fr-rect" | "subprocess" | "subproc" | "framed-rectangle" => "subroutine"
"cyl" | "db" | "database" => "cylinder"
"data-store" => "datastore"
"directory" => "folder"
"circ" => "circle"
"diam" | "decision" | "question" => "diamond"
"hex" | "prepare" => "hexagon"
"lean-right" | "in-out" | "lean_right" => "lean-r"
"lean-left" | "out-in" | "lean_left" => "lean-l"
"priority" | "trapezoid-bottom" | "trapezoid" => "trap-b"
"manual" | "trapezoid-top" | "inv-trapezoid" | "inv_trapezoid" => "trap-t"
"dbl-circ" | "double-circle" => "doublecircle"
"card" | "notched-rectangle" => "notch-rect"
"lined-rectangle" | "lined-process" | "lin-proc" | "shaded-process" =>
"lin-rect"
"start" | "small-circle" | "stateStart" => "sm-circ"
"stop" | "framed-circle" | "stateEnd" => "fr-circ"
"join" | "forkJoin" => "fork"
"collate" => "hourglass"
"comment" | "brace-l" => "brace"
"com-link" | "lightning-bolt" => "bolt"
"document" => "doc"
"half-rounded-rectangle" => "delay"
"das" | "horizontal-cylinder" => "h-cyl"
"disk" | "lined-cylinder" => "lin-cyl"
"curved-trapezoid" | "display" => "curv-trap"
"div-proc" | "divided-rectangle" | "divided-process" => "div-rect"
"extract" | "triangle" => "tri"
"internal-storage" | "window-pane" => "win-pane"
"junction" | "filled-circle" => "f-circ"
"loop-limit" | "notched-pentagon" => "notch-pent"
"manual-file" | "flipped-triangle" => "flip-tri"
"manual-input" | "sloped-rectangle" => "sl-rect"
"documents" | "st-doc" | "stacked-document" => "docs"
"procs" | "processes" | "stacked-rectangle" => "st-rect"
"stored-data" | "bow-tie-rectangle" => "bow-rect"
"summary" | "crossed-circle" => "cross-circ"
"tagged-document" => "tag-doc"
"tagged-rectangle" | "tag-proc" | "tagged-process" => "tag-rect"
"paper-tape" => "flag"
"rect_left_inv_arrow" => "odd"
"lined-document" => "lin-doc"
"doublecircle" => "doublecircle"
"parallelogram" => "lean-r"
"parallelogram-alt" => "lean-l"
"asymmetric" => "odd"
_ => value
}
}
///|
pub let built_in_shapes : ReadOnlyArray[String] = [
"rect", "ellipse", "rounded", "stadium", "subroutine", "cylinder", "datastore",
"folder", "bucket", "console", "browser", "person", "circle", "bang", "cloud",
"diamond", "hexagon", "lean-r", "lean-l", "trap-b", "trap-t", "doublecircle", "text",
"notch-rect", "lin-rect", "sm-circ", "fr-circ", "fork", "hourglass", "brace", "brace-r",
"braces", "bolt", "doc", "delay", "h-cyl", "lin-cyl", "curv-trap", "div-rect",
"tri", "win-pane", "f-circ", "notch-pent", "flip-tri", "sl-rect", "docs", "st-rect",
"bow-rect", "cross-circ", "tag-doc", "tag-rect", "flag", "odd", "lin-doc", "state",
"choice", "note", "composite", "anchor",
]
///|
pub fn validate(
model : Flowchart,
nodes_limit~ : Int32 = 10000,
edges_limit~ : Int32 = 50000,
depth_limit~ : Int32 = 256,
work_limit~ : Int32 = 1000000,
) -> ReadOnlyArray[@diagrams.Diagnostic] {
let out = []
let ids : Map[String, Bool] = Map([])
let lanes : Map[String, Bool] = Map([])
let groups : Map[String, Group] = Map([])
let mut work = 0
let mut halted = false
fn finding(code : String, value : String) {
if out.length() < 256 {
out.push(@diagrams.Diagnostic::error("flowchart/" + code, value))
}
}
fn limit() {
if !halted {
halted = true
let diagnostic = @diagrams.Diagnostic::error(
"flowchart/model-limit", "Model validation limit exceeded",
)
if out.length() >= 256 {
out[255] = diagnostic
} else {
out.push(diagnostic)
}
}
}
fn charge(units : Int32) -> Bool {
if halted {
return false
}
if units < 0 || work_limit < 0 || units > work_limit - work {
limit()
false
} else {
work += units
true
}
}
fn text(value : String) -> Bool {
charge(value.length())
}
fn optional(value : String?) -> Bool {
match value {
Some(value) => text(value)
None => true
}
}
fn metadata(values : ReadOnlyArray[(String, String)]) -> Bool {
if !charge(values.length()) {
return false
}
for pair in values {
if !text(pair.0) || !text(pair.1) {
return false
}
}
true
}
fn properties(value : RenderProperties?) -> Bool {
match value {
Some(value) => optional(value.link) && metadata(value.metadata)
None => true
}
}
fn origin(value : SourceBinding?) -> Bool {
guard value is Some(value) else { return true }
if !text(value.format) || !text(value.path) {
return false
}
if value.span is Some(span) &&
(
span.start.offset < 0 ||
span.end.offset < span.start.offset ||
span.start.line < 1 ||
span.start.column < 1 ||
span.end.line < span.start.line ||
span.end.column < 1 ||
(
span.start.line == span.end.line &&
span.end.column < span.start.column
)
) {
finding("source-binding", "Invalid source span: " + value.path)
}
true
}
fn add(id : String) {
if !text(id) || !charge(1) {
return
}
if id == "" || ids.contains(id) {
finding("duplicate-id", "Ambiguous element ID: " + id)
}
ids[id] = true
}
if nodes_limit <= 0 || edges_limit <= 0 || depth_limit <= 0 || work_limit <= 0 {
finding("invalid-limits", "Flowchart limits must be positive")
return ReadOnlyArray::from_array(out[:])
}
if !charge(1) {
return ReadOnlyArray::from_array(out[:])
}
let mut remaining = nodes_limit
for
count in [
model.nodes.length(),
model.lanes.length(),
model.groups.length(),
model.notes.length(),
model.regions.length(),
] {
if count > remaining {
limit()
return ReadOnlyArray::from_array(out[:])
}
remaining -= count
}
if model.connections.length() > edges_limit {
limit()
return ReadOnlyArray::from_array(out[:])
}
if !text(model.id) ||
!optional(model.title) ||
!optional(model.accessibility_title) ||
!optional(model.accessibility_description) {
return ReadOnlyArray::from_array(out[:])
}
for lane in model.lanes {
add(lane.id)
if !text(lane.label) {
return ReadOnlyArray::from_array(out[:])
}
lanes[lane.id] = true
}
for group in model.groups {
add(group.id)
if !text(group.label) || !optional(group.parent) || !optional(group.lane) {
return ReadOnlyArray::from_array(out[:])
}
groups[group.id] = group
}
for node in model.nodes {
add(node.id)
if halted {
return ReadOnlyArray::from_array(out[:])
}
}
let endpoints = ids.copy()
for note in model.notes {
add(note.id)
if halted {
return ReadOnlyArray::from_array(out[:])
}
endpoints[note.id] = true
}
let regions : Map[String, Region] = Map([])
for region in model.regions {
add(region.id)
regions[region.id] = region
if !optional(region.parent) ||
!optional(region.entry) ||
!optional(region.exit) ||
!optional(region.condition) ||
!optional(region.join_specification) ||
!origin(region.origin) ||
!charge(region.members.length() + region.branches.length()) {
return ReadOnlyArray::from_array(out[:])
}
for branch in region.branches {
if remaining <= 0 {
limit()
return ReadOnlyArray::from_array(out[:])
}
remaining -= 1
add(branch.id)
if !text(branch.label) ||
!optional(branch.condition) ||
!origin(branch.origin) ||
!charge(branch.members.length()) {
return ReadOnlyArray::from_array(out[:])
}
}
}
for region in model.regions {
let seen : Map[String, Bool] = Map([])
let mut cursor = Some(region.id)
let mut depth = 0
while cursor is Some(id) {
if !charge(1) {
return ReadOnlyArray::from_array(out[:])
}
if depth >= depth_limit {
limit()
return ReadOnlyArray::from_array(out[:])
}
depth += 1
if seen.contains(id) {
finding("region-cycle", "Control region cycle at " + id)
break
}
seen[id] = true
match regions.get(id) {
Some(parent) => cursor = parent.parent
None => {
finding("region-reference", "Missing control region " + id)
break
}
}
}
for id in region.entry.iter().concat(region.exit.iter()) {
if !text(id) {
return ReadOnlyArray::from_array(out[:])
}
if !endpoints.contains(id) {
finding("region-reference", "Missing control endpoint " + id)
}
}
for id in region.members {
if !text(id) {
return ReadOnlyArray::from_array(out[:])
}
if !endpoints.contains(id) && !regions.contains(id) {
finding("region-reference", "Missing control member " + id)
}
}
for branch in region.branches {
for id in branch.members {
if !text(id) {
return ReadOnlyArray::from_array(out[:])
}
if !endpoints.contains(id) && !regions.contains(id) {
finding("region-reference", "Missing branch member " + id)
}
}
}
}
let connection_ids : Map[String, Bool] = Map([])
for edge in model.connections {
if !text(edge.id) {
return ReadOnlyArray::from_array(out[:])
}
connection_ids[edge.id] = true
}
for note in model.notes {
for id in note.connection_targets {
if !text(id) {
return ReadOnlyArray::from_array(out[:])
}
if !connection_ids.contains(id) {
finding("note-target", "Missing note connection " + id)
}
}
if !text(note.text) ||
!text(note.placement) ||
!optional(note.lane) ||
!origin(note.origin) ||
!charge(
note.targets.length() +
note.connection_targets.length() +
note.groups.length(),
) {
return ReadOnlyArray::from_array(out[:])
}
if !["left", "right", "top", "bottom", "floating"].contains(note.placement) {
finding("note-placement", "Invalid note placement: " + note.id)
}
for target in note.targets {
if !text(target) {
return ReadOnlyArray::from_array(out[:])
}
if !endpoints.contains(target) {
finding("note-target", "Missing note target " + target)
}
}
if note.lane is Some(lane) && !lanes.contains(lane) {
finding("ownership", "Missing note lane: " + note.id)
}
if note.groups.length() > depth_limit {
limit()
return ReadOnlyArray::from_array(out[:])
}
let mut parent : String? = None
for id in note.groups {
if !text(id) {
return ReadOnlyArray::from_array(out[:])
}
match groups.get(id) {
Some(group) => {
if group.parent != parent ||
(group.lane != None && group.lane != note.lane) {
finding("ownership", "Inconsistent note ownership: " + note.id)
}
parent = Some(id)
}
None => finding("ownership", "Missing note group " + id)
}
}
}
for group in model.groups {
if group.lane is Some(lane) && !lanes.contains(lane) {
finding("ownership", "Missing lane for group " + group.id)
}
if group.parent is Some(parent) &&
groups.get(parent) is Some(parent_group) &&
parent_group.lane != None &&
parent_group.lane != group.lane {
finding("ownership", "Group and parent have different lanes: " + group.id)
}
let seen : Map[String, Bool] = Map([])
let mut cursor = Some(group.id)
let mut depth = 0
while cursor is Some(id) {
if !charge(1) || depth >= depth_limit {
limit()
return ReadOnlyArray::from_array(out[:])
}
depth += 1
if seen.contains(id) {
finding("ownership-cycle", "Group ownership cycle at " + id)
break
}
seen[id] = true
match groups.get(id) {
Some(parent) => cursor = parent.parent
None => {
finding("ownership", "Missing group " + id)
break
}
}
}
}
for node in model.nodes {
if !origin(node.origin) || !charge(node.regions.length()) {
return ReadOnlyArray::from_array(out[:])
}
for id in node.regions {
if !text(id) {
return ReadOnlyArray::from_array(out[:])
}
if !regions.contains(id) {
finding("region-reference", "Missing node region " + id)
}
}
if !text(node.label) ||
!text(node.shape) ||
!optional(node.lane) ||
!metadata(node.metadata) ||
!properties(node.properties) ||
!charge(node.groups.length()) {
return ReadOnlyArray::from_array(out[:])
}
if node.groups.length() > depth_limit {
limit()
return ReadOnlyArray::from_array(out[:])
}
if node.lane is Some(lane) && !lanes.contains(lane) {
finding("ownership", "Missing lane for node " + node.id)
}
let mut parent : String? = None
for id in node.groups {
if !text(id) {
return ReadOnlyArray::from_array(out[:])
}
match groups.get(id) {
Some(group) => {
if group.parent != parent ||
(group.lane != None && group.lane != node.lane) {
finding("ownership", "Inconsistent group path for node " + node.id)
}
parent = Some(id)
}
None => finding("ownership", "Missing group " + id)
}
}
}
let edge_ids : Map[String, Bool] = Map([])
for edge in model.connections {
if !origin(edge.origin) {
return ReadOnlyArray::from_array(out[:])
}
if !text(edge.id) ||
!text(edge.source) ||
!text(edge.target) ||
!text(edge.label) ||
!charge(1) {
return ReadOnlyArray::from_array(out[:])
}
if edge_ids.contains(edge.id) || edge.id == "" {
finding("duplicate-edge-id", "Ambiguous connection ID: " + edge.id)
}
edge_ids[edge.id] = true
if !endpoints.contains(edge.source) {
finding(
"missing-endpoint",
"Missing source " + edge.source + " of " + edge.id,
)
}
if !endpoints.contains(edge.target) {
finding(
"missing-endpoint",
"Missing target " + edge.target + " of " + edge.id,
)
}
if edge.min_length < 1 {
finding(
"invalid-length",
"Connection length must be positive: " + edge.id,
)
}
}
ReadOnlyArray::from_array(out[:])
}
///|
pub extend Direction with Eq::{equal, not_equal}
///|
pub extend Direction with @debug.Debug::{to_repr}
///|
pub extend LabelKind with Eq::{equal, not_equal}
///|
pub extend LabelKind with @debug.Debug::{to_repr}
///|
pub extend LineStyle with Eq::{equal, not_equal}
///|
pub extend LineStyle with @debug.Debug::{to_repr}
///|
pub extend Marker with Eq::{equal, not_equal}
///|
pub extend Marker with @debug.Debug::{to_repr}
///|
pub extend FlowDirection with Eq::{equal, not_equal}
///|
pub extend FlowDirection with @debug.Debug::{to_repr}
///|
pub extend RenderProperties with Eq::{equal, not_equal}
///|
pub extend RenderProperties with @debug.Debug::{to_repr}
///|
pub extend Lane with Eq::{equal, not_equal}
///|
pub extend Lane with @debug.Debug::{to_repr}
///|
pub extend Group with Eq::{equal, not_equal}
///|
pub extend Group with @debug.Debug::{to_repr}
///|
pub extend Node with Eq::{equal, not_equal}
///|
pub extend Node with @debug.Debug::{to_repr}
///|
pub extend Connection with Eq::{equal, not_equal}
///|
pub extend Connection with @debug.Debug::{to_repr}
///|
pub extend Flowchart with Eq::{equal, not_equal}
///|
pub extend Flowchart with @debug.Debug::{to_repr}
///|
pub let diagnostic_codes : ReadOnlyArray[String] = [
"flowchart/duplicate-id", "flowchart/duplicate-edge-id", "flowchart/ownership",
"flowchart/ownership-cycle", "flowchart/missing-endpoint", "flowchart/invalid-length",
"flowchart/model-limit", "flowchart/invalid-limits", "flowchart/id-limit", "flowchart/source-binding",
"flowchart/region-cycle", "flowchart/region-reference", "flowchart/note-placement",
"flowchart/note-target",
]
///|
pub extend SourceBinding with Eq::{equal, not_equal}
///|
pub extend SourceBinding with @debug.Debug::{to_repr}
///|
pub extend Role with Eq::{equal, not_equal}
///|
pub extend Role with @debug.Debug::{to_repr}
///|
pub extend ConnectionRole with Eq::{equal, not_equal}
///|
pub extend ConnectionRole with @debug.Debug::{to_repr}
///|
pub extend RegionKind with Eq::{equal, not_equal}
///|
pub extend RegionKind with @debug.Debug::{to_repr}
///|
pub extend Branch with Eq::{equal, not_equal}
///|
pub extend Branch with @debug.Debug::{to_repr}
///|
pub extend Region with Eq::{equal, not_equal}
///|
pub extend Region with @debug.Debug::{to_repr}
///|
pub extend Note with Eq::{equal, not_equal}
///|
pub extend Note with @debug.Debug::{to_repr}