summaryrefslogtreecommitdiffstats
path: root/doc
diff options
context:
space:
mode:
authorQuentin Young <qlyoung@cumulusnetworks.com>2018-02-14 00:09:58 +0100
committerQuentin Young <qlyoung@cumulusnetworks.com>2018-02-14 00:09:58 +0100
commite53d58537c6a42a4c80ba9561d6c081b25a182cd (patch)
treefb7e125e30d9271522fbe8d2d341ee43a522d66b /doc
parentdoc: add everything to makefile (diff)
downloadfrr-e53d58537c6a42a4c80ba9561d6c081b25a182cd.tar.xz
frr-e53d58537c6a42a4c80ba9561d6c081b25a182cd.zip
doc: document CLI BNF grammar, add DFA figures
Technical details on CLI implementation. Signed-off-by: Quentin Young <qlyoung@cumulusnetworks.com>
Diffstat (limited to 'doc')
-rw-r--r--doc/developer/cli.rst161
-rw-r--r--doc/figures/cligraph.svg211
2 files changed, 311 insertions, 61 deletions
diff --git a/doc/developer/cli.rst b/doc/developer/cli.rst
index 2ecb70e93..cff3d21ef 100644
--- a/doc/developer/cli.rst
+++ b/doc/developer/cli.rst
@@ -66,10 +66,58 @@ parser is implemented in Bison and the lexer in Flex. These may be found in
to look. Bison is very stable and if it detects a syntax error, 99% of
the time it will be a syntax error in your definition.
+The formal grammar in BNF is given below. This is the grammar implemented in
+the Bison parser. At runtime, the Bison parser reads all of the CLI strings and
+builds a combined directed graph that is used to match and interpret user
+input.
+
+Human-friendly explanations of how to use this grammar are given a bit later in
+this section alongside information on the :ref:`cli-data-structures` constructed
+by the parser.
+
+.. productionlist::
+ command: `cmd_token_seq`
+ : `cmd_token_seq` `placeholder_token` "..."
+ cmd_token_seq: *empty*
+ : `cmd_token_seq` `cmd_token`
+ cmd_token: `simple_token`
+ : `selector`
+ simple_token: `literal_token`
+ : `placeholder_token`
+ literal_token: WORD `varname_token`
+ varname_token: "$" WORD
+ placeholder_token: `placeholder_token_real` `varname_token`
+ placeholder_token_real: IPV4
+ : IPV4_PREFIX
+ : IPV6
+ : IPV6_PREFIX
+ : VARIABLE
+ : RANGE
+ : MAC
+ : MAC_PREFIX
+ selector: "<" `selector_seq_seq` ">" `varname_token`
+ : "{" `selector_seq_seq` "}" `varname_token`
+ : "[" `selector_seq_seq` "]" `varname_token`
+ selector_seq_seq: `selector_seq_seq` "|" `selector_token_seq`
+ : `selector_token_seq`
+ selector_token_seq: `selector_token_seq` `selector_token`
+ : `selector_token`
+ selector_token: `selector`
+ : `simple_token`
+
Tokens
~~~~~~
-
-Each element in a command definition is assigned a type by the parser based on a set of regular expression rules.
+The various capitalized tokens in the BNF above are in fact themselves
+placeholders, but not defined as such in the formal grammar; the grammar
+provides the structure, and the tokens are actually more like a type system for
+the strings you write in your CLI definitions. A CLI definition string is
+broken apart and each piece is assigned a type by the lexer based on a set of
+regular expressions. The parser uses the type information to verify the string
+and determine the structure of the CLI graph; additional metadata (such as the
+raw text of each token) is encoded into the graph as it is constructed by the
+parser, but this is merely a dumb copy job.
+
+Here is a brief summary of the various token types along with examples.
+-----------------+-----------------+-------------------------------------------------------------+
| Token type | Syntax | Description |
@@ -382,6 +430,8 @@ In the examples below, each arrowed token needs a doc string.
"command <foo|bar> [example]"
^ ^ ^ ^
+.. _cli-data-structures:
+
Data Structures
---------------
@@ -401,76 +451,65 @@ self-contained 'subgraphs'. Each subgraph is a tree except that all of
the 'leaves' actually share a child node. This helps with minimizing
graph size and debugging.
-As an example, the subgraph generated by looks like this:
+As a working example, here is the graph of the following command: ::
+
+ show [ip] bgp neighbors [<A.B.C.D|X:X::X:X|WORD>] [json]
+
+.. figure:: ../figures/cligraph.svg
+ :align: center
+
+ Graph of example CLI command
-::
- .
- .
- |
- +----+---+
- +--- -+ FORK +----+
- | +--------+ |
- +--v---+ +--v---+
- | foo | | bar |
- +--+---+ +--+---+
- | +------+ |
- +------> JOIN <-----+
- +---+--+
- |
- .
- .
-
-FORK and JOIN nodes are plumbing nodes that don't correspond to user
+``FORK`` and ``JOIN`` nodes are plumbing nodes that don't correspond to user
input. They're necessary in order to deduplicate these constructs where
applicable.
-Options follow the same form, except that there is an edge from the FORK
-node to the JOIN node.
+Options follow the same form, except that there is an edge from the ``FORK``
+node to the ``JOIN`` node. Since all of the subgraphs in the example command
+are optional, all of them have this edge.
-Keywords follow the same form, except that there is an edge from JOIN to
-FORK. Because of this the CLI graph cannot be called acyclic. There is
-special logic in the input matching code that keeps a stack of paths
-already taken through the node in order to disallow following the same
-path more than once.
+Keywords follow the same form, except that there is an edge from ``JOIN`` to
+``FORK``. Because of this the CLI graph cannot be called acyclic. There is
+special logic in the input matching code that keeps a stack of paths already
+taken through the node in order to disallow following the same path more than
+once.
-Variadics are a bit special; they have an edge back to themselves, which
-allows repeating the same input indefinitely.
+Variadics are a bit special; they have an edge back to themselves, which allows
+repeating the same input indefinitely.
-The leaves of the graph are nodes that have no out edges. These nodes
-are special; their data section does not contain a token, as most nodes
-do, or NULL, as in FORK/JOIN nodes, but instead has a pointer to a
+The leaves of the graph are nodes that have no out edges. These nodes are
+special; their data section does not contain a token, as most nodes do, or
+NULL, as in ``FORK``/``JOIN`` nodes, but instead has a pointer to a
cmd\_element. All paths through the graph that terminate on a leaf are
guaranteed to be defined by that command. When a user enters a complete
-command, the command matcher tokenizes the input and executes a DFS on
-the CLI graph. If it is simultaneously able to exhaust all input (one
-input token per graph node), and then find exactly one leaf connected to
-the last node it reaches, then the input has matched the corresponding
-command and the command is executed. If it finds more than one node,
-then the command is ambiguous (more on this in deduplication). If it
-cannot exhaust all input, the command is unknown. If it exhausts all
-input but does not find an edge node, the command is incomplete.
-
-The parser uses an incremental strategy to build the CLI graph for a
-node. Each command is parsed into its own graph, and then this graph is
-merged into the overall graph. During this merge step, the parser makes
-a best-effort attempt to remove duplicate nodes. If it finds a node in
-the overall graph that is equal to a node in the corresponding position
-in the command graph, it will intelligently merge the properties from
-the node in the command graph into the already-existing node. Subgraphs
-are also checked for isomorphism and merged where possible. The
-definition of whether two nodes are 'equal' is based on the equality of
-some set of token properties; read the parser source for the most
+command, the command matcher tokenizes the input and executes a DFS on the CLI
+graph. If it is simultaneously able to exhaust all input (one input token per
+graph node), and then find exactly one leaf connected to the last node it
+reaches, then the input has matched the corresponding command and the command
+is executed. If it finds more than one node, then the command is ambiguous
+(more on this in deduplication). If it cannot exhaust all input, the command is
+unknown. If it exhausts all input but does not find an edge node, the command
+is incomplete.
+
+The parser uses an incremental strategy to build the CLI graph for a node. Each
+command is parsed into its own graph, and then this graph is merged into the
+overall graph. During this merge step, the parser makes a best-effort attempt
+to remove duplicate nodes. If it finds a node in the overall graph that is
+equal to a node in the corresponding position in the command graph, it will
+intelligently merge the properties from the node in the command graph into the
+already-existing node. Subgraphs are also checked for isomorphism and merged
+where possible. The definition of whether two nodes are 'equal' is based on the
+equality of some set of token properties; read the parser source for the most
up-to-date definition of equality.
-When the parser is unable to deduplicate some complicated constructs,
-this can result in two identical paths through separate parts of the
-graph. If this occurs and the user enters input that matches these
-paths, they will receive an 'ambiguous command' error and will be unable
-to execute the command. Most of the time the parser can detect and warn
-about duplicate commands, but it will not always be able to do this.
-Hence care should be taken before defining a new command to ensure it is
-not defined elsewhere.
+When the parser is unable to deduplicate some complicated constructs, this can
+result in two identical paths through separate parts of the graph. If this
+occurs and the user enters input that matches these paths, they will receive an
+'ambiguous command' error and will be unable to execute the command. Most of
+the time the parser can detect and warn about duplicate commands, but it will
+not always be able to do this. Hence care should be taken before defining a
+new command to ensure it is not defined elsewhere.
Command handlers
----------------
@@ -481,7 +520,7 @@ this:
::
- int (*func) (const struct cmd_element *, struct vty *, int, struct cmd_token *[]);
+ int (*func) (const struct cmd_element *, struct vty *, int, struct cmd_token *[]);
The first argument is the command definition struct. The last argument
is an ordered array of tokens that correspond to the path taken through
diff --git a/doc/figures/cligraph.svg b/doc/figures/cligraph.svg
new file mode 100644
index 000000000..a1dd01702
--- /dev/null
+++ b/doc/figures/cligraph.svg
@@ -0,0 +1,211 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.38.0 (20140413.2041)
+ -->
+<!-- Title: %3 Pages: 1 -->
+<svg width="300pt" height="980pt"
+ viewBox="0.00 0.00 299.50 980.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 976)">
+<title>%3</title>
+<polygon fill="white" stroke="none" points="-4,4 -4,-976 295.5,-976 295.5,4 -4,4"/>
+<!-- n0xd46960 -->
+<g id="node1" class="node"><title>n0xd46960</title>
+<polygon fill="#ccffcc" stroke="black" points="158,-972 86,-972 86,-936 158,-936 158,-972"/>
+<text text-anchor="start" x="94" y="-952.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">START_TKN</text>
+</g>
+<!-- n0xd46be0 -->
+<g id="node2" class="node"><title>n0xd46be0</title>
+<polygon fill="#ffffff" stroke="black" points="159,-900 85,-900 85,-864 159,-864 159,-900"/>
+<text text-anchor="start" x="93" y="-885.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">WORD_TKN</text>
+<text text-anchor="start" x="101.5" y="-875.2" font-family="Fira Mono" font-size="9.00">&quot;</text>
+<text text-anchor="start" x="105.5" y="-875.2" font-family="Fira Mono" font-weight="bold" font-size="11.00" fill="#0055ff">show</text>
+<text text-anchor="start" x="138.5" y="-875.2" font-family="Fira Mono" font-size="9.00">&quot;</text>
+</g>
+<!-- n0xd46960&#45;&gt;n0xd46be0 -->
+<g id="edge1" class="edge"><title>n0xd46960&#45;&gt;n0xd46be0</title>
+<path fill="none" stroke="black" d="M122,-935.697C122,-927.983 122,-918.712 122,-910.112"/>
+<polygon fill="black" stroke="black" points="125.5,-910.104 122,-900.104 118.5,-910.104 125.5,-910.104"/>
+</g>
+<!-- n0xd47f80 -->
+<g id="node3" class="node"><title>n0xd47f80</title>
+<polygon fill="#aaddff" stroke="black" points="156.5,-828 87.5,-828 87.5,-792 156.5,-792 156.5,-828"/>
+<text text-anchor="start" x="95.5" y="-808.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">FORK_TKN</text>
+</g>
+<!-- n0xd46be0&#45;&gt;n0xd47f80 -->
+<g id="edge2" class="edge"><title>n0xd46be0&#45;&gt;n0xd47f80</title>
+<path fill="none" stroke="black" d="M122,-863.697C122,-855.983 122,-846.712 122,-838.112"/>
+<polygon fill="black" stroke="black" points="125.5,-838.104 122,-828.104 118.5,-838.104 125.5,-838.104"/>
+</g>
+<!-- n0xd47c70 -->
+<g id="node4" class="node"><title>n0xd47c70</title>
+<polygon fill="#ffffff" stroke="black" points="127,-756 53,-756 53,-720 127,-720 127,-756"/>
+<text text-anchor="start" x="61" y="-741.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">WORD_TKN</text>
+<text text-anchor="start" x="80.5" y="-731.2" font-family="Fira Mono" font-size="9.00">&quot;</text>
+<text text-anchor="start" x="84.5" y="-731.2" font-family="Fira Mono" font-weight="bold" font-size="11.00" fill="#0055ff">ip</text>
+<text text-anchor="start" x="95.5" y="-731.2" font-family="Fira Mono" font-size="9.00">&quot;</text>
+</g>
+<!-- n0xd47f80&#45;&gt;n0xd47c70 -->
+<g id="edge3" class="edge"><title>n0xd47f80&#45;&gt;n0xd47c70</title>
+<path fill="none" stroke="black" d="M114.09,-791.697C110.447,-783.728 106.046,-774.1 102.006,-765.264"/>
+<polygon fill="black" stroke="black" points="105.16,-763.744 97.8191,-756.104 98.7936,-766.654 105.16,-763.744"/>
+</g>
+<!-- n0xd484c0 -->
+<g id="node5" class="node"><title>n0xd484c0</title>
+<polygon fill="#ddaaff" stroke="black" points="153.5,-684 90.5,-684 90.5,-648 153.5,-648 153.5,-684"/>
+<text text-anchor="start" x="98.5" y="-664.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">JOIN_TKN</text>
+</g>
+<!-- n0xd47f80&#45;&gt;n0xd484c0 -->
+<g id="edge20" class="edge"><title>n0xd47f80&#45;&gt;n0xd484c0</title>
+<path fill="none" stroke="black" d="M127.824,-791.56C130.931,-781.33 134.431,-768.08 136,-756 138.06,-740.133 138.06,-735.867 136,-720 134.897,-711.506 132.839,-702.434 130.634,-694.24"/>
+<polygon fill="black" stroke="black" points="133.945,-693.087 127.824,-684.44 127.216,-695.017 133.945,-693.087"/>
+</g>
+<!-- n0xd47c70&#45;&gt;n0xd484c0 -->
+<g id="edge4" class="edge"><title>n0xd47c70&#45;&gt;n0xd484c0</title>
+<path fill="none" stroke="black" d="M97.9101,-719.697C101.553,-711.728 105.954,-702.1 109.994,-693.264"/>
+<polygon fill="black" stroke="black" points="113.206,-694.654 114.181,-684.104 106.84,-691.744 113.206,-694.654"/>
+</g>
+<!-- n0xd47ca0 -->
+<g id="node6" class="node"><title>n0xd47ca0</title>
+<polygon fill="#ffffff" stroke="black" points="159,-612 85,-612 85,-576 159,-576 159,-612"/>
+<text text-anchor="start" x="93" y="-597.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">WORD_TKN</text>
+<text text-anchor="start" x="106.5" y="-587.2" font-family="Fira Mono" font-size="9.00">&quot;</text>
+<text text-anchor="start" x="110.5" y="-587.2" font-family="Fira Mono" font-weight="bold" font-size="11.00" fill="#0055ff">bgp</text>
+<text text-anchor="start" x="133.5" y="-587.2" font-family="Fira Mono" font-size="9.00">&quot;</text>
+</g>
+<!-- n0xd484c0&#45;&gt;n0xd47ca0 -->
+<g id="edge5" class="edge"><title>n0xd484c0&#45;&gt;n0xd47ca0</title>
+<path fill="none" stroke="black" d="M122,-647.697C122,-639.983 122,-630.712 122,-622.112"/>
+<polygon fill="black" stroke="black" points="125.5,-622.104 122,-612.104 118.5,-622.104 125.5,-622.104"/>
+</g>
+<!-- n0xd48540 -->
+<g id="node7" class="node"><title>n0xd48540</title>
+<polygon fill="#ffffff" stroke="black" points="164.5,-540 79.5,-540 79.5,-504 164.5,-504 164.5,-540"/>
+<text text-anchor="start" x="93" y="-525.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">WORD_TKN</text>
+<text text-anchor="start" x="87.5" y="-515.2" font-family="Fira Mono" font-size="9.00">&quot;</text>
+<text text-anchor="start" x="91.5" y="-515.2" font-family="Fira Mono" font-weight="bold" font-size="11.00" fill="#0055ff">neighbors</text>
+<text text-anchor="start" x="152.5" y="-515.2" font-family="Fira Mono" font-size="9.00">&quot;</text>
+</g>
+<!-- n0xd47ca0&#45;&gt;n0xd48540 -->
+<g id="edge6" class="edge"><title>n0xd47ca0&#45;&gt;n0xd48540</title>
+<path fill="none" stroke="black" d="M122,-575.697C122,-567.983 122,-558.712 122,-550.112"/>
+<polygon fill="black" stroke="black" points="125.5,-550.104 122,-540.104 118.5,-550.104 125.5,-550.104"/>
+</g>
+<!-- n0xd490c0 -->
+<g id="node8" class="node"><title>n0xd490c0</title>
+<polygon fill="#aaddff" stroke="black" points="156.5,-468 87.5,-468 87.5,-432 156.5,-432 156.5,-468"/>
+<text text-anchor="start" x="95.5" y="-448.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">FORK_TKN</text>
+</g>
+<!-- n0xd48540&#45;&gt;n0xd490c0 -->
+<g id="edge7" class="edge"><title>n0xd48540&#45;&gt;n0xd490c0</title>
+<path fill="none" stroke="black" d="M122,-503.697C122,-495.983 122,-486.712 122,-478.112"/>
+<polygon fill="black" stroke="black" points="125.5,-478.104 122,-468.104 118.5,-478.104 125.5,-478.104"/>
+</g>
+<!-- n0xd48fc0 -->
+<g id="node9" class="node"><title>n0xd48fc0</title>
+<polygon fill="#ffffff" stroke="black" points="64,-396 0,-396 0,-360 64,-360 64,-396"/>
+<text text-anchor="start" x="8" y="-380.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">IPV4_TKN</text>
+<text text-anchor="start" x="15" y="-371.8" font-family="Fira Mono" font-size="9.00">A.B.C.D</text>
+</g>
+<!-- n0xd490c0&#45;&gt;n0xd48fc0 -->
+<g id="edge8" class="edge"><title>n0xd490c0&#45;&gt;n0xd48fc0</title>
+<path fill="none" stroke="black" d="M99.7528,-431.697C88.4181,-422.881 74.4698,-412.032 62.1811,-402.474"/>
+<polygon fill="black" stroke="black" points="64.0336,-399.481 53.9913,-396.104 59.736,-405.007 64.0336,-399.481"/>
+</g>
+<!-- n0xd491e0 -->
+<g id="node10" class="node"><title>n0xd491e0</title>
+<polygon fill="#ddaaff" stroke="black" points="153.5,-324 90.5,-324 90.5,-288 153.5,-288 153.5,-324"/>
+<text text-anchor="start" x="98.5" y="-304.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">JOIN_TKN</text>
+</g>
+<!-- n0xd490c0&#45;&gt;n0xd491e0 -->
+<g id="edge19" class="edge"><title>n0xd490c0&#45;&gt;n0xd491e0</title>
+<path fill="none" stroke="black" d="M117.536,-431.953C115.065,-421.63 112.248,-408.153 111,-396 109.366,-380.084 109.366,-375.916 111,-360 111.877,-351.455 113.531,-342.255 115.294,-333.958"/>
+<polygon fill="black" stroke="black" points="118.743,-334.573 117.536,-324.047 111.915,-333.028 118.743,-334.573"/>
+</g>
+<!-- n0xd49340 -->
+<g id="node15" class="node"><title>n0xd49340</title>
+<polygon fill="#ffffff" stroke="black" points="184,-396 120,-396 120,-360 184,-360 184,-396"/>
+<text text-anchor="start" x="128" y="-380.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">IPV6_TKN</text>
+<text text-anchor="start" x="135" y="-371.8" font-family="Fira Mono" font-size="9.00">X:X::X:X</text>
+</g>
+<!-- n0xd490c0&#45;&gt;n0xd49340 -->
+<g id="edge15" class="edge"><title>n0xd490c0&#45;&gt;n0xd49340</title>
+<path fill="none" stroke="black" d="M129.416,-431.697C132.794,-423.813 136.87,-414.304 140.623,-405.546"/>
+<polygon fill="black" stroke="black" points="143.947,-406.675 144.67,-396.104 137.513,-403.917 143.947,-406.675"/>
+</g>
+<!-- n0xd49480 -->
+<g id="node16" class="node"><title>n0xd49480</title>
+<polygon fill="#ffffff" stroke="black" points="291.5,-396 202.5,-396 202.5,-360 291.5,-360 291.5,-396"/>
+<text text-anchor="start" x="210.5" y="-380.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">VARIABLE_TKN</text>
+<text text-anchor="start" x="233" y="-371.8" font-family="Fira Mono" font-size="9.00">WORD</text>
+</g>
+<!-- n0xd490c0&#45;&gt;n0xd49480 -->
+<g id="edge17" class="edge"><title>n0xd490c0&#45;&gt;n0xd49480</title>
+<path fill="none" stroke="black" d="M152.578,-431.876C169.074,-422.639 189.624,-411.131 207.336,-401.212"/>
+<polygon fill="black" stroke="black" points="209.289,-404.13 216.304,-396.19 205.869,-398.022 209.289,-404.13"/>
+</g>
+<!-- n0xd48fc0&#45;&gt;n0xd491e0 -->
+<g id="edge9" class="edge"><title>n0xd48fc0&#45;&gt;n0xd491e0</title>
+<path fill="none" stroke="black" d="M54.2472,-359.697C65.5819,-350.881 79.5302,-340.032 91.8189,-330.474"/>
+<polygon fill="black" stroke="black" points="94.264,-333.007 100.009,-324.104 89.9664,-327.481 94.264,-333.007"/>
+</g>
+<!-- n0xd496e0 -->
+<g id="node11" class="node"><title>n0xd496e0</title>
+<polygon fill="#aaddff" stroke="black" points="156.5,-252 87.5,-252 87.5,-216 156.5,-216 156.5,-252"/>
+<text text-anchor="start" x="95.5" y="-232.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">FORK_TKN</text>
+</g>
+<!-- n0xd491e0&#45;&gt;n0xd496e0 -->
+<g id="edge10" class="edge"><title>n0xd491e0&#45;&gt;n0xd496e0</title>
+<path fill="none" stroke="black" d="M122,-287.697C122,-279.983 122,-270.712 122,-262.112"/>
+<polygon fill="black" stroke="black" points="125.5,-262.104 122,-252.104 118.5,-262.104 125.5,-262.104"/>
+</g>
+<!-- n0xd495e0 -->
+<g id="node12" class="node"><title>n0xd495e0</title>
+<polygon fill="#ffffff" stroke="black" points="127,-180 53,-180 53,-144 127,-144 127,-180"/>
+<text text-anchor="start" x="61" y="-165.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">WORD_TKN</text>
+<text text-anchor="start" x="73.5" y="-155.2" font-family="Fira Mono" font-size="9.00">&quot;</text>
+<text text-anchor="start" x="77.5" y="-155.2" font-family="Fira Mono" font-weight="bold" font-size="11.00" fill="#0055ff">json</text>
+<text text-anchor="start" x="102.5" y="-155.2" font-family="Fira Mono" font-size="9.00">&quot;</text>
+</g>
+<!-- n0xd496e0&#45;&gt;n0xd495e0 -->
+<g id="edge11" class="edge"><title>n0xd496e0&#45;&gt;n0xd495e0</title>
+<path fill="none" stroke="black" d="M114.09,-215.697C110.447,-207.728 106.046,-198.1 102.006,-189.264"/>
+<polygon fill="black" stroke="black" points="105.16,-187.744 97.8191,-180.104 98.7936,-190.654 105.16,-187.744"/>
+</g>
+<!-- n0xd497c0 -->
+<g id="node13" class="node"><title>n0xd497c0</title>
+<polygon fill="#ddaaff" stroke="black" points="153.5,-108 90.5,-108 90.5,-72 153.5,-72 153.5,-108"/>
+<text text-anchor="start" x="98.5" y="-88.8" font-family="Fira Mono" font-weight="bold" font-size="9.00">JOIN_TKN</text>
+</g>
+<!-- n0xd496e0&#45;&gt;n0xd497c0 -->
+<g id="edge14" class="edge"><title>n0xd496e0&#45;&gt;n0xd497c0</title>
+<path fill="none" stroke="black" d="M127.824,-215.56C130.931,-205.33 134.431,-192.08 136,-180 138.06,-164.133 138.06,-159.867 136,-144 134.897,-135.506 132.839,-126.434 130.634,-118.24"/>
+<polygon fill="black" stroke="black" points="133.945,-117.087 127.824,-108.44 127.216,-119.017 133.945,-117.087"/>
+</g>
+<!-- n0xd495e0&#45;&gt;n0xd497c0 -->
+<g id="edge12" class="edge"><title>n0xd495e0&#45;&gt;n0xd497c0</title>
+<path fill="none" stroke="black" d="M97.9101,-143.697C101.553,-135.728 105.954,-126.1 109.994,-117.264"/>
+<polygon fill="black" stroke="black" points="113.206,-118.654 114.181,-108.104 106.84,-115.744 113.206,-118.654"/>
+</g>
+<!-- end0xd49900 -->
+<g id="node14" class="node"><title>end0xd49900</title>
+<polygon fill="#ffddaa" stroke="black" points="149,-36 95,-36 95,-0 149,-0 149,-36"/>
+<text text-anchor="start" x="112.5" y="-15.8" font-family="Fira Mono" font-size="9.00">end</text>
+</g>
+<!-- n0xd497c0&#45;&gt;end0xd49900 -->
+<g id="edge13" class="edge"><title>n0xd497c0&#45;&gt;end0xd49900</title>
+<path fill="none" stroke="black" d="M122,-71.6966C122,-63.9827 122,-54.7125 122,-46.1124"/>
+<polygon fill="black" stroke="black" points="125.5,-46.1043 122,-36.1043 118.5,-46.1044 125.5,-46.1043"/>
+</g>
+<!-- n0xd49340&#45;&gt;n0xd491e0 -->
+<g id="edge16" class="edge"><title>n0xd49340&#45;&gt;n0xd491e0</title>
+<path fill="none" stroke="black" d="M144.584,-359.697C141.206,-351.813 137.13,-342.304 133.377,-333.546"/>
+<polygon fill="black" stroke="black" points="136.487,-331.917 129.33,-324.104 130.053,-334.675 136.487,-331.917"/>
+</g>
+<!-- n0xd49480&#45;&gt;n0xd491e0 -->
+<g id="edge18" class="edge"><title>n0xd49480&#45;&gt;n0xd491e0</title>
+<path fill="none" stroke="black" d="M216.422,-359.876C199.926,-350.639 179.376,-339.131 161.664,-329.212"/>
+<polygon fill="black" stroke="black" points="163.131,-326.022 152.696,-324.19 159.711,-332.13 163.131,-326.022"/>
+</g>
+</g>
+</svg>