sourced-component 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +862 -0
- data/Rakefile +8 -0
- data/examples/env.rb +78 -0
- data/examples/tree.rb +221 -0
- data/lib/sourced/component/dsl.rb +26 -0
- data/lib/sourced/component/env_provider.rb +167 -0
- data/lib/sourced/component/errors.rb +19 -0
- data/lib/sourced/component/events.rb +111 -0
- data/lib/sourced/component/graph.rb +60 -0
- data/lib/sourced/component/implementation.rb +83 -0
- data/lib/sourced/component/injector.rb +59 -0
- data/lib/sourced/component/mermaid.rb +36 -0
- data/lib/sourced/component/notifier.rb +44 -0
- data/lib/sourced/component/tree.rb +117 -0
- data/lib/sourced/component/version.rb +7 -0
- data/lib/sourced/component.rb +952 -0
- metadata +100 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sourced
|
|
4
|
+
class Component
|
|
5
|
+
# Returned by Component#graph. #components are hashes describing each declared component, by full path:
|
|
6
|
+
# {
|
|
7
|
+
# key: 'sourced.db', # the component's full path from the root
|
|
8
|
+
# type: <Plumb type>,
|
|
9
|
+
# type_name: 'Interface[exec]',
|
|
10
|
+
# implemented: true,
|
|
11
|
+
# mode: :singleton, # nil if not implemented
|
|
12
|
+
# status: :built,
|
|
13
|
+
# deps: ['logger'], # full paths of the components this one depends on
|
|
14
|
+
# missing: [], # deps that aren't declared, or are namespaces without an implementation
|
|
15
|
+
# dependents: ['app'], # components in the graph that depend on this one
|
|
16
|
+
# provider: <provider> # the provider, or the block of #config!/#config. nil for blocks of hooks
|
|
17
|
+
# }
|
|
18
|
+
class Graph < Data.define(:status, :components)
|
|
19
|
+
# Mermaid classes for component statuses, plus declared-but-unimplemented components,
|
|
20
|
+
# missing dependencies, and dependencies outside the graph (ex. a library's graph, with an app's overrides)
|
|
21
|
+
MERMAID_CLASSES = Mermaid::STATUS_CLASSES.merge(
|
|
22
|
+
missing: 'fill:#fee2e2,stroke:#dc2626,stroke-dasharray:4 3',
|
|
23
|
+
external: 'fill:#ffffff,stroke:#a1a1aa,stroke-dasharray:2 2'
|
|
24
|
+
).freeze
|
|
25
|
+
|
|
26
|
+
# A Mermaid flowchart of the dependency graph.
|
|
27
|
+
# Edges point from each dependency to its dependents (build and start order).
|
|
28
|
+
# Singletons are rectangles, dynamic components are rounded, and nodes are styled by status.
|
|
29
|
+
# Unimplemented components, missing dependencies and dependencies outside the graph are included, with dashed borders.
|
|
30
|
+
def to_mermaid
|
|
31
|
+
ids = {}
|
|
32
|
+
id_for = ->(key) { ids[key] ||= "c#{ids.size}" }
|
|
33
|
+
lines = ['flowchart LR']
|
|
34
|
+
|
|
35
|
+
components.each do |node|
|
|
36
|
+
details = Mermaid.details(node[:implemented], node[:mode], node[:status], node[:deferred])
|
|
37
|
+
label = "#{Mermaid.escape(node[:key])}<br/>#{Mermaid.escape(node[:type_name])}<br/><i>#{details}</i>"
|
|
38
|
+
open, close = Mermaid.component_shape(node[:mode])
|
|
39
|
+
css_class = node[:implemented] ? node[:status] : :unimplemented
|
|
40
|
+
lines << " #{id_for.(node[:key])}#{open}#{label}#{close}:::#{css_class}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
keys = components.map { |node| node[:key] }
|
|
44
|
+
missing = components.flat_map { |node| node.fetch(:missing, []) }.uniq
|
|
45
|
+
outside = components.flat_map { |node| node[:deps] }.uniq - keys
|
|
46
|
+
outside.each do |key|
|
|
47
|
+
details, css_class = missing.include?(key) ? ['not declared', :missing] : ['outside this component', :external]
|
|
48
|
+
lines << " #{id_for.(key)}[\"#{Mermaid.escape(key)}<br/><i>#{details}</i>\"]:::#{css_class}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
components.each do |node|
|
|
52
|
+
node[:deps].each { |dep| lines << " #{id_for.(dep)} --> #{id_for.(node[:key])}" }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
lines.concat(Mermaid.class_defs(MERMAID_CLASSES))
|
|
56
|
+
lines.join("\n")
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sourced
|
|
4
|
+
class Component
|
|
5
|
+
# singleton: built once, on #build!, and memoized
|
|
6
|
+
# dynamic: built on every read
|
|
7
|
+
# alias: reads another component (its only dep). Memoized if that component is, see Component#alias
|
|
8
|
+
MODES = %i[singleton dynamic alias].freeze
|
|
9
|
+
|
|
10
|
+
# How a node is built. Deps are keys relative to the implementer: the component that called #component! or #component.
|
|
11
|
+
# prepare: hooks run with no arguments
|
|
12
|
+
# build: hooks run with dep values. The last result is the node's value
|
|
13
|
+
# start: hooks run with (value, context). Again after each stop, for a component started by key
|
|
14
|
+
# stop: hooks run with (value), when a started component is stopped, or torn down
|
|
15
|
+
# teardown: hooks run with (value), once, when the root is torn down
|
|
16
|
+
# A dep ending in '.*' depends on every component directly under that key, and its value is
|
|
17
|
+
# a hash of their values by key segment, ex. 'reactors.*' => { 'foo' => <Foo>, 'bar' => <Bar> }
|
|
18
|
+
class Implementation
|
|
19
|
+
WILDCARD = /\A[^.*]+(\.[^.*]+)*\.\*\z/
|
|
20
|
+
|
|
21
|
+
# provider: the provider the component was implemented with (see Component#component!), the block of
|
|
22
|
+
# Component#config! and #config, or nil for blocks of hooks
|
|
23
|
+
attr_reader :deps, :implementer, :mode, :provider
|
|
24
|
+
|
|
25
|
+
def self.from_block(deps, implementer:, mode:, &block)
|
|
26
|
+
dsl = DSL.new
|
|
27
|
+
if block
|
|
28
|
+
block.arity > 0 ? block.call(dsl) : dsl.instance_eval(&block)
|
|
29
|
+
end
|
|
30
|
+
new(deps, implementer:, mode:, hooks: dsl.hooks)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# An alias of the component at +target+, relative to the implementer: its value is the target's,
|
|
34
|
+
# and it has no other hooks
|
|
35
|
+
def self.alias(target, implementer:)
|
|
36
|
+
target = target.to_s
|
|
37
|
+
raise ArgumentError, "can't alias #{target.inspect}: an alias takes a single component, not a wildcard" if target.include?('*')
|
|
38
|
+
|
|
39
|
+
new([target], implementer:, mode: :alias, hooks: DSL.new.build { |value| value }.hooks)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Hooks that a provider's builder can implement, besides #call (the build step)
|
|
43
|
+
OPTIONAL_HOOKS = %i[prepare start stop teardown].freeze
|
|
44
|
+
|
|
45
|
+
# From a provider's builder: #call(*deps) is the build step, and it can also implement
|
|
46
|
+
# #prepare, #start(value, context), #stop(value) and #teardown(value).
|
|
47
|
+
def self.from_builder(builder, deps, implementer:, mode:, provider: nil)
|
|
48
|
+
dsl = DSL.new
|
|
49
|
+
dsl.build(builder)
|
|
50
|
+
OPTIONAL_HOOKS.each do |name|
|
|
51
|
+
dsl.public_send(name, builder.method(name)) if builder.respond_to?(name)
|
|
52
|
+
end
|
|
53
|
+
new(deps, implementer:, mode:, hooks: dsl.hooks, provider:)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def initialize(deps, implementer:, mode:, hooks:, provider: nil)
|
|
57
|
+
raise ArgumentError, "unknown mode #{mode.inspect}, expected one of #{MODES.join(', ')}" unless MODES.include?(mode)
|
|
58
|
+
|
|
59
|
+
@deps = deps.map { |d| d.to_s.freeze }.freeze
|
|
60
|
+
@deps.each do |dep|
|
|
61
|
+
next unless dep.include?('*')
|
|
62
|
+
next if dep.match?(WILDCARD)
|
|
63
|
+
|
|
64
|
+
raise ArgumentError, "invalid dependency #{dep.inspect}: a wildcard must be the last segment, ex. 'reactors.*'"
|
|
65
|
+
end
|
|
66
|
+
@implementer = implementer
|
|
67
|
+
@mode = mode
|
|
68
|
+
@provider = provider
|
|
69
|
+
@hooks = hooks.transform_values(&:freeze).freeze
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def singleton? = mode == :singleton
|
|
73
|
+
def dynamic? = mode == :dynamic
|
|
74
|
+
def alias? = mode == :alias
|
|
75
|
+
|
|
76
|
+
def prepare = @hooks[:prepare].each(&:call)
|
|
77
|
+
def build(*deps) = @hooks[:build].reduce(nil) { |_, b| b.call(*deps) }
|
|
78
|
+
def start(value, context) = @hooks[:start].each { |b| b.call(value, context) }
|
|
79
|
+
def stop(value) = @hooks[:stop].each { |b| b.call(value) }
|
|
80
|
+
def teardown(value) = @hooks[:teardown].each { |b| b.call(value) }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sourced
|
|
4
|
+
class Component
|
|
5
|
+
# A module that injects components into a class as keyword arguments to #initialize,
|
|
6
|
+
# defaulting to the component's value when the object is instantiated.
|
|
7
|
+
# include App.inject('logger', 'sourced.store' => 'st')
|
|
8
|
+
# Each include prepends its own #initialize, which takes its kwargs and passes the rest on to super,
|
|
9
|
+
# so multiple injections (and the class' own #initialize) compose.
|
|
10
|
+
# Including it raises InjectionError if the class already has a method named like an injected reader.
|
|
11
|
+
# Values are read from the nodes themselves, so a class injecting from a library's component
|
|
12
|
+
# gets the overrides of the application that mounts it.
|
|
13
|
+
class Injector < Module
|
|
14
|
+
attr_reader :names
|
|
15
|
+
|
|
16
|
+
# nodes: { 'component.key' => <Component node> }
|
|
17
|
+
# names: { 'component.key' => :kwarg_name }
|
|
18
|
+
def initialize(nodes, names)
|
|
19
|
+
super()
|
|
20
|
+
@names = names.freeze
|
|
21
|
+
|
|
22
|
+
# Instance variable names are computed once, not on every #new
|
|
23
|
+
entries = names.map { |key, name| [nodes.fetch(key), name, :"@#{name}"] }.freeze
|
|
24
|
+
|
|
25
|
+
initializer = Module.new do
|
|
26
|
+
define_method(:initialize) do |*args, **kwargs, &block|
|
|
27
|
+
entries.each do |node, name, ivar|
|
|
28
|
+
instance_variable_set(ivar, kwargs.key?(name) ? kwargs.delete(name) : node.read)
|
|
29
|
+
end
|
|
30
|
+
super(*args, **kwargs, &block)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Checked before the module is added to the class, so a refused include leaves the class untouched
|
|
35
|
+
define_singleton_method(:append_features) do |base|
|
|
36
|
+
taken = (base.ancestors.grep(Injector) - [self]).flat_map { |i| i.names.values } & names.values
|
|
37
|
+
raise InjectionError, "#{base} already injects #{taken.join(', ')}" if taken.any?
|
|
38
|
+
|
|
39
|
+
# Readers would silently replace these, including private ones (ex. Kernel#format)
|
|
40
|
+
defined = names.values.select { |name| base.method_defined?(name) || base.private_method_defined?(name) }
|
|
41
|
+
if defined.any?
|
|
42
|
+
methods = defined.map { |name| "##{name} (from #{base.instance_method(name).owner})" }
|
|
43
|
+
raise InjectionError, "#{base} already defines #{methods.join(', ')}: " \
|
|
44
|
+
"inject under another name instead, ex. inject('key' => 'other_name')"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
super(base)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
define_singleton_method(:included) do |base|
|
|
51
|
+
base.prepend(initializer)
|
|
52
|
+
base.attr_reader(*names.values)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def inspect = "#<#{self.class} #{names.map { |key, name| "#{key} => #{name}" }.join(', ')}>"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sourced
|
|
4
|
+
class Component
|
|
5
|
+
# Helpers shared by Graph#to_mermaid and Tree#to_mermaid
|
|
6
|
+
module Mermaid
|
|
7
|
+
# Styles for component statuses, and for declared-but-unimplemented components
|
|
8
|
+
STATUS_CLASSES = {
|
|
9
|
+
open: 'fill:#f4f4f5,stroke:#71717a',
|
|
10
|
+
prepared: 'fill:#e0f2fe,stroke:#0284c7',
|
|
11
|
+
built: 'fill:#ede9fe,stroke:#7c3aed',
|
|
12
|
+
started: 'fill:#dcfce7,stroke:#16a34a',
|
|
13
|
+
stopped: 'fill:#ffedd5,stroke:#ea580c',
|
|
14
|
+
torn_down: 'fill:#e4e4e7,stroke:#52525b,color:#52525b',
|
|
15
|
+
unimplemented: 'fill:#fef9c3,stroke:#ca8a04,stroke-dasharray:4 3'
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
# Escape label text, so type names with brackets, pipes or quotes are safe
|
|
19
|
+
def self.escape(text)
|
|
20
|
+
text.to_s.gsub('&', '#amp;').gsub('"', '#quot;').gsub('<', '#lt;').gsub('>', '#gt;')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# ex. 'singleton, started', 'singleton, built, deferred', or 'not implemented'
|
|
24
|
+
def self.details(implemented, mode, status, deferred = false)
|
|
25
|
+
return 'not implemented' unless implemented
|
|
26
|
+
|
|
27
|
+
[mode, status, ('deferred' if deferred)].compact.join(', ')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Open and close brackets: singletons are rectangles, dynamic components are rounded
|
|
31
|
+
def self.component_shape(mode) = mode == :dynamic ? ['(["', '"])'] : ['["', '"]']
|
|
32
|
+
|
|
33
|
+
def self.class_defs(classes) = classes.map { |name, style| " classDef #{name} #{style}" }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sourced
|
|
4
|
+
class Component
|
|
5
|
+
# The default notifier. Custom notifiers must implement the same #publish and #subscribe interface.
|
|
6
|
+
# Handlers are called synchronously, in the order they subscribed, by the thread or fiber running the lifecycle step.
|
|
7
|
+
# Errors raised by handlers propagate to the caller.
|
|
8
|
+
class Notifier
|
|
9
|
+
def initialize
|
|
10
|
+
@subscriptions = [].freeze
|
|
11
|
+
@lock = Mutex.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Subscribe to an event type string (ex. 'components.built'),
|
|
15
|
+
# or an event class, which also matches its subclasses (ex. Events::ComponentEvent for all component events)
|
|
16
|
+
def subscribe(event_class_or_type, &handler)
|
|
17
|
+
raise ArgumentError, 'a handler block is required' unless handler
|
|
18
|
+
|
|
19
|
+
matcher = case event_class_or_type
|
|
20
|
+
when Class
|
|
21
|
+
->(event) { event.is_a?(event_class_or_type) }
|
|
22
|
+
when String, Symbol
|
|
23
|
+
type = event_class_or_type.to_s
|
|
24
|
+
raise ArgumentError, "unknown event type #{type}" unless Event.registry[type]
|
|
25
|
+
|
|
26
|
+
->(event) { event.type == type }
|
|
27
|
+
else
|
|
28
|
+
raise ArgumentError, "can't subscribe to #{event_class_or_type.inspect}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# copy-on-write, so publishing never needs the lock
|
|
32
|
+
@lock.synchronize { @subscriptions = [*@subscriptions, [matcher, handler]].freeze }
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def publish(event)
|
|
37
|
+
@subscriptions.each { |matcher, handler| handler.call(event) if matcher.call(event) }
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
NotifierInterface = Plumb::Types::Interface[:publish, :subscribe]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sourced
|
|
4
|
+
class Component
|
|
5
|
+
# Returned by Component#tree: the tree of components under a component, as nested nodes.
|
|
6
|
+
# Unlike Component#graph (the dependency graph), it shows how components are nested,
|
|
7
|
+
# which components are mounted, and who declared and implemented each node.
|
|
8
|
+
# tree = App.tree
|
|
9
|
+
# tree.status # => :built, the root's status
|
|
10
|
+
# tree.root # => a Tree::Node, with #children
|
|
11
|
+
# puts tree # => an ASCII tree
|
|
12
|
+
# tree.to_h # => nested hashes
|
|
13
|
+
class Tree < Data.define(:status, :root)
|
|
14
|
+
# key: the node's segment, ex. 'db'. nil for the root of a tree
|
|
15
|
+
# path: the full path from the root, ex. 'sourced.db'. nil for the root of a tree
|
|
16
|
+
# namespace: whether it has no type and no implementation
|
|
17
|
+
# mounted: whether it's a component mounted here (it owns itself)
|
|
18
|
+
# deferred: whether the root's start! skips it (see Component#defer)
|
|
19
|
+
# owner: the full path of the component that declared it. nil for the root
|
|
20
|
+
# implementer: the full path of the component that implemented it. nil for the root, or if not implemented
|
|
21
|
+
class Node < Data.define(
|
|
22
|
+
:key, :path, :type, :type_name, :namespace, :mounted, :implemented, :mode, :status, :deferred, :owner, :implementer, :children
|
|
23
|
+
)
|
|
24
|
+
# Whether it's implemented by a component other than the one that declared it, ex. an app overriding a library's component
|
|
25
|
+
def overridden? = implemented && owner != implementer
|
|
26
|
+
|
|
27
|
+
def to_h = super.merge(children: children.map(&:to_h))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_h = { status:, root: root.to_h }
|
|
31
|
+
|
|
32
|
+
# (root)
|
|
33
|
+
# ├── logger Interface[info] (singleton, built)
|
|
34
|
+
# ├── sourced [mounted]
|
|
35
|
+
# │ ├── logger Interface[info] (singleton, built)
|
|
36
|
+
# │ └── db DB (singleton, built) implemented by (root)
|
|
37
|
+
# └── cache
|
|
38
|
+
# └── redis String (singleton, built)
|
|
39
|
+
def to_s
|
|
40
|
+
lines = [label(root, root.path || '(root)')]
|
|
41
|
+
render(root.children, '', lines)
|
|
42
|
+
lines.join("\n")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Mermaid classes for component statuses and unimplemented components, plus namespaces and roots
|
|
46
|
+
MERMAID_CLASSES = Mermaid::STATUS_CLASSES.merge(
|
|
47
|
+
namespace: 'fill:#ffffff,stroke:#a1a1aa',
|
|
48
|
+
root: 'fill:#fafafa,stroke:#18181b,stroke-width:2px'
|
|
49
|
+
).freeze
|
|
50
|
+
|
|
51
|
+
# A top-down Mermaid flowchart of the tree, with an edge from each node to its children.
|
|
52
|
+
# Roots (the root of the tree, and mounted components) are hexagons, namespaces are rounded,
|
|
53
|
+
# and components are styled like Graph#to_mermaid: singletons are rectangles, dynamic components
|
|
54
|
+
# are rounded, colored by status. Components implemented by another component say which one.
|
|
55
|
+
# flowchart TD
|
|
56
|
+
# n0{{"(root)"}}:::root
|
|
57
|
+
# n1["logger<br/>Interface[info]<br/><i>singleton, built</i>"]:::built
|
|
58
|
+
# n2{{"sourced"}}:::root
|
|
59
|
+
# n3["db<br/>DB<br/><i>singleton, built</i><br/><i>implemented by (root)</i>"]:::built
|
|
60
|
+
# n0 --> n1
|
|
61
|
+
# n0 --> n2
|
|
62
|
+
# n2 --> n3
|
|
63
|
+
def to_mermaid
|
|
64
|
+
nodes = []
|
|
65
|
+
edges = []
|
|
66
|
+
add = lambda do |node, name, parent_id, subtree_root|
|
|
67
|
+
id = "n#{nodes.size}"
|
|
68
|
+
nodes << " #{id}#{mermaid_node(node, name, subtree_root)}"
|
|
69
|
+
edges << " #{parent_id} --> #{id}" if parent_id
|
|
70
|
+
node.children.each { |child| add.(child, child.key, id, child.mounted) }
|
|
71
|
+
end
|
|
72
|
+
add.(root, root.path || '(root)', nil, true)
|
|
73
|
+
|
|
74
|
+
['flowchart TD', *nodes, *edges, *Mermaid.class_defs(MERMAID_CLASSES)].join("\n")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private def mermaid_node(node, name, subtree_root)
|
|
78
|
+
lines = [Mermaid.escape(name)]
|
|
79
|
+
unless node.namespace
|
|
80
|
+
lines << Mermaid.escape(node.type_name)
|
|
81
|
+
lines << "<i>#{Mermaid.details(node.implemented, node.mode, node.status, node.deferred)}</i>"
|
|
82
|
+
end
|
|
83
|
+
lines << "<i>implemented by #{Mermaid.escape(node.implementer || '(root)')}</i>" if node.overridden?
|
|
84
|
+
label = lines.join('<br/>')
|
|
85
|
+
|
|
86
|
+
css_class = if node.namespace then subtree_root ? :root : :namespace
|
|
87
|
+
elsif node.implemented then node.status
|
|
88
|
+
else :unimplemented
|
|
89
|
+
end
|
|
90
|
+
open, close = if subtree_root then ['{{"', '"}}']
|
|
91
|
+
elsif node.namespace then ['("', '")']
|
|
92
|
+
else Mermaid.component_shape(node.mode)
|
|
93
|
+
end
|
|
94
|
+
"#{open}#{label}#{close}:::#{css_class}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private def render(nodes, prefix, lines)
|
|
98
|
+
nodes.each_with_index do |node, i|
|
|
99
|
+
last = i == nodes.size - 1
|
|
100
|
+
lines << "#{prefix}#{last ? '└── ' : '├── '}#{label(node, node.key)}"
|
|
101
|
+
render(node.children, prefix + (last ? ' ' : '│ '), lines)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private def label(node, name)
|
|
106
|
+
parts = [name]
|
|
107
|
+
parts << '[mounted]' if node.mounted
|
|
108
|
+
unless node.namespace
|
|
109
|
+
parts << node.type_name
|
|
110
|
+
parts << "(#{[node.implemented ? node.mode : 'not implemented', node.status, ('deferred' if node.deferred)].compact.join(', ')})"
|
|
111
|
+
end
|
|
112
|
+
parts << "implemented by #{node.implementer || '(root)'}" if node.overridden?
|
|
113
|
+
parts.join(' ')
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|