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.
data/README.md ADDED
@@ -0,0 +1,862 @@
1
+ # Sourced::Component
2
+
3
+ Configuration as a tree of typed components, with dependencies and a managed lifecycle.
4
+
5
+ Every node in the tree is a `Sourced::Component`. A node can declare a type, be implemented with dependencies and lifecycle hooks (`prepare`, `build`, `start`, `stop`, `teardown`), and have subcomponents of its own. Libraries declare their own root components; applications mount them under a namespace, and implement or override their subcomponents.
6
+
7
+ ```ruby
8
+ require 'sourced/component'
9
+
10
+ App = Sourced::Component.new
11
+
12
+ # 1. Declare what the component has, and the types values must satisfy
13
+ App.declare('db.url', String) { 'sqlite://app.db' } # with a default implementation
14
+ App.declare('db', DB)
15
+
16
+ # 2. Implement components, with dependencies and lifecycle hooks
17
+ App.component!('db', ['db.url']) do
18
+ build { |url| DB.new(url) }
19
+ start { |db, _context| db.connect }
20
+ teardown { |db| db.disconnect }
21
+ end
22
+
23
+ # 3. Boot
24
+ App.start!
25
+
26
+ # 4. Read values, or inject them into your classes
27
+ App['db'] # => #<DB ...>
28
+
29
+ class Repo
30
+ include App.inject('db')
31
+ end
32
+ Repo.new.db # => #<DB ...>
33
+
34
+ # 5. Shut down
35
+ App.teardown!
36
+ ```
37
+
38
+ ## Installation
39
+
40
+ Add the gem to your application's Gemfile:
41
+
42
+ ```ruby
43
+ gem 'sourced-component'
44
+ ```
45
+
46
+ Requires Ruby 3.2+. Types are [Plumb](https://github.com/ismasan/plumb) types.
47
+
48
+ ## Declaring components
49
+
50
+ `#declare(key, type = Any, &default)` adds a typed node to the tree. Values are parsed through the declared type when they're built, so a component that builds the wrong thing fails the boot.
51
+
52
+ ```ruby
53
+ T = Sourced::Component::T # Plumb::Types
54
+
55
+ App.declare('logger', T::Interface[:info, :debug])
56
+ App.declare('settings.retries', Integer)
57
+ App.declare('cache', T::Interface[:get, :set].nullable) # optional, by type
58
+ App.declare('anything') # Any
59
+ ```
60
+
61
+ A block is a default implementation: a singleton built once, with no dependencies. It can be replaced with `#component!` or `#component`.
62
+
63
+ ```ruby
64
+ App.declare('logger', T::Interface[:info]) { Logger.new($stdout) }
65
+ ```
66
+
67
+ ### Nested keys
68
+
69
+ Dot-separated keys build the tree. Missing intermediate segments are created as *namespaces*: nodes without a type or an implementation, which are skipped by the lifecycle.
70
+
71
+ ```ruby
72
+ App.declare('sourced.db.logger', Logger)
73
+ # App
74
+ # └── sourced (namespace)
75
+ # └── db (namespace)
76
+ # └── logger (Logger)
77
+ ```
78
+
79
+ A component can keep declaring under the nodes it created, and can give a namespace it created a type later, so it becomes a component with a value of its own:
80
+
81
+ ```ruby
82
+ App.declare('db.url', String) { 'sqlite://app.db' }
83
+ App.declare('db', DB) # 'db' was a namespace, now it's a component too
84
+ App.component!('db', ['db.url']) { build { |url| DB.new(url) } }
85
+ ```
86
+
87
+ Declaring a key twice raises `DeclarationOverrideError`.
88
+
89
+ ## Implementing components
90
+
91
+ `#component!(key, deps = [], provider = nil, &block)` implements a declared node as a singleton, with a block of lifecycle hooks or a [provider](#providers). Dependencies are keys of other components, and their values are passed to `build`, in order.
92
+
93
+ ```ruby
94
+ App.component!('db', ['db.url', 'logger']) do
95
+ prepare { require 'sequel' } # before anything is built
96
+ build { |url, logger| Sequel.connect(url, logger:) } # returns the component's value
97
+ start { |db, context| } # after everything is built
98
+ stop { |db| } # when it's stopped, and on shutdown, in reverse order
99
+ teardown { |db| db.disconnect } # once, on shutdown, after stop
100
+ end
101
+ ```
102
+
103
+ `start` and `stop` pair up, and can run more than once for the same value when a component is [stopped and started again by key](#deferred-components-and-starting-and-stopping-by-key). `teardown` runs once, when the root is torn down.
104
+
105
+ All hooks are optional. Hooks can also be any callable, and the block can take the DSL as an argument instead of being evaluated in it:
106
+
107
+ ```ruby
108
+ App.component!('clock') { build(-> { Time }) }
109
+ App.component!('db', ['db.url']) { |c| c.build { |url| DB.new(url) } }
110
+ ```
111
+
112
+ Implementing a node again replaces its implementation (the last one wins), including declared defaults. Dependencies can be declared after the component that uses them: they're resolved when the component is prepared.
113
+
114
+ ### Singleton and dynamic components
115
+
116
+ - `#component!` implements a singleton: built once on `#build!`, and memoized.
117
+ - `#component` implements a dynamic component: built on every read, ex. a per-request value. Dynamic components take the same deps and hooks, and still go through `prepare`, `start` and `teardown`, with a `nil` value.
118
+
119
+ ```ruby
120
+ App.declare('request_id', String)
121
+ App.component('request_id') { build { SecureRandom.uuid } }
122
+
123
+ App['request_id'] # => "8d1c..."
124
+ App['request_id'] # => "f30a..."
125
+ ```
126
+
127
+ ### Configs
128
+
129
+ `#config!` and `#config` are shortcuts for components that only have a build step. The block builds the value, and gets the dependencies' values:
130
+
131
+ ```ruby
132
+ App.config!('foo.bar') { 10 } # singleton
133
+ App.config!('with.deps', ['sourced.db']) { |db| Foo.new(db) } # singleton, with deps
134
+ App.config('now') { Time.now } # dynamic: built on every read
135
+ ```
136
+
137
+ They're the same as:
138
+
139
+ ```ruby
140
+ App.component!('with.deps', ['sourced.db']) { build { |db| Foo.new(db) } }
141
+ App.component('now') { build { Time.now } }
142
+ ```
143
+
144
+ ### Aliases
145
+
146
+ `#alias(key, target)` implements a declared node as an alias of another component: reading it reads the target. It's useful to wire one library's component to another's:
147
+
148
+ ```ruby
149
+ App.mount('sourced', Sourced)
150
+ App.mount('sidereal', Sidereal)
151
+ App.alias('sidereal.store', 'sourced.store') # sidereal uses sourced's store
152
+ ```
153
+
154
+ It's like `App.config!('sidereal.store', ['sourced.store']) { |store| store }`, with a few differences:
155
+
156
+ - An alias follows its target's mode: an alias of a singleton is memoized, so it's the same object as the target, and an alias of a dynamic component is built on every read.
157
+ - An alias has no hooks. The target runs its own lifecycle, and the alias is a dependent like any other: it starts after the target, and waits for it if it's [deferred](#deferred-components-and-starting-and-stopping-by-key).
158
+ - The target's value is parsed through the alias' declared type, so a type mismatch raises `Plumb::ParseError` naming the alias.
159
+ - The target is a key relative to the implementing component, like any dependency. Wildcards aren't allowed (`ArgumentError`), and an alias of a namespace raises `MissingDependencyError` on `#prepare!`.
160
+
161
+ Aliases show their mode as `alias` in [`#tree`](#tree) and [`#graph`](#graph), with the target as their dependency.
162
+
163
+ ### Wildcard dependencies
164
+
165
+ A dependency ending in `.*` depends on every component directly under that key, so components can be registered under a namespace without listing them anywhere else. Its value is a hash of their values, by key segment:
166
+
167
+ ```ruby
168
+ App.declare('reactors.foo', Foo) { Foo.new }
169
+ App.declare('reactors.bar', Bar) { Bar.new }
170
+
171
+ App.declare('runner', Runner)
172
+ App.config!('runner', ['logger', 'reactors.*']) do |logger, reactors|
173
+ Runner.new(logger, reactors) # reactors => { 'foo' => <Foo>, 'bar' => <Bar> }
174
+ end
175
+ ```
176
+
177
+ - The components under the key are dependencies like any other: they're prepared, built and started before the component that depends on them, and torn down after it.
178
+ - Wildcards are resolved on `#prepare!`, once the tree is locked, so components declared after the dependent are included.
179
+ - Only direct children are included. Nested namespaces are skipped (`reactors.nested.deep` isn't included), but implemented components are included even if they have children of their own.
180
+ - If nothing is under the key, the value is an empty hash.
181
+ - Like other dependencies, wildcards are [relative to the implementing component](#dependencies-are-relative-to-the-implementing-component).
182
+ - The wildcard can only be the last segment: `reactors.*.foo` and `reactors.f*` raise `ArgumentError`.
183
+ - A library can let the applications that mount it register components under one of its namespaces: see [Extension points](#extension-points).
184
+
185
+ ### Providers
186
+
187
+ Instead of a block, `#component!` and `#component` take a provider that implements the component. A provider is either:
188
+
189
+ - **a callable**, called with the dependencies' values as the build step:
190
+
191
+ ```ruby
192
+ class DBFactory
193
+ def self.call(url) = DB.new(url)
194
+ end
195
+
196
+ App.component!('db', ['db.url'], DBFactory)
197
+ App.component!('clock', -> { Time }) # no dependencies
198
+ App.component('request_id', -> { SecureRandom.uuid }) # dynamic
199
+ ```
200
+
201
+ - **or an object with `#builder_for(node)`**, which returns that callable for the node it's implementing. Use it for providers that need to know about the node, ex. its `type` or `path`. `Sourced::Component::ENVProvider` is one:
202
+
203
+ ```ruby
204
+ App.component!('user.email', Sourced::Component::ENVProvider.new('USER_EMAIL'))
205
+ App.component!('user.info', Sourced::Component::ENVProvider.new(/^USER_/, :downcase))
206
+ App.component!('everything', Sourced::Component::ENVProvider) # all variables
207
+ ```
208
+
209
+ The callable (the provider itself, or what `#builder_for` returns) can also implement any of `#prepare`, `#start(value, context)`, `#stop(value)` and `#teardown(value)`, which become the component's other lifecycle hooks. Hooks it leaves out are skipped, so plain lambdas only build. A provider can supply a whole lifecycle:
210
+
211
+ ```ruby
212
+ class PoolProvider
213
+ def self.builder_for(node) = new(node)
214
+
215
+ def initialize(node) = @node = node
216
+ def call(url) = Pool.new(url, name: @node.path) # build
217
+ def start(pool, _context) = pool.connect
218
+ def teardown(pool) = pool.close
219
+ end
220
+
221
+ App.component!('db.pool', ['db.url'], PoolProvider)
222
+ ```
223
+
224
+ Provided values are parsed through the declared type, like any other. Passing both a provider and a block raises `ArgumentError`, and so do a provider that responds to neither `#call` nor `#builder_for`, and a `#builder_for` that doesn't return a callable.
225
+
226
+ ### ENV components
227
+
228
+ `#env` implements singleton components built from ENV variables. Values are decoded into each declared type with [`Plumb::Codec::Forms`](https://github.com/ismasan/plumb), the codec for string input (`'3000'` → `3000`, `'true'` → `true`, `'1977-11-29'` → a `Date`). It maps ENV variables, or regexes matching them, to component keys:
229
+
230
+ ```ruby
231
+ # ENV: USER_EMAIL=me@example.com USER_NAME=Ismael USER_DOB=1977-11-29 APP_PORT=3000
232
+
233
+ App.declare('user.email', T::Email)
234
+ App.declare('app.port', Integer)
235
+ App.declare('user.info', T::Data[name: String, dob: Date])
236
+
237
+ # Single variables
238
+ App.env('USER_EMAIL' => 'user.email', 'APP_PORT' => 'app.port')
239
+
240
+ # Variables matching a regex, collected into a hash and decoded into a struct
241
+ App.env(:downcase, /^USER_/ => 'user.info')
242
+
243
+ App.start!
244
+ App['user.email'] # => "me@example.com"
245
+ App['app.port'] # => 3000
246
+ App['user.info'] # => #<User name="Ismael" dob=1977-11-29>
247
+ ```
248
+
249
+ | Call | Reads |
250
+ | --- | --- |
251
+ | `env('USER_EMAIL' => 'user.email')` | the `USER_EMAIL` variable |
252
+ | `env(/^USER_/ => 'user.info')` | variables matching the regex, into a hash, with the match removed: `USER_NAME` → `NAME` |
253
+ | `env(:downcase, /^USER_/ => 'user.info')` | the same, with modifiers applied to the names: `USER_NAME` → `name` |
254
+ | `env('user.info')` | all variables, into a hash |
255
+ | `env(:downcase, 'user.info')` | all variables, with modifiers |
256
+
257
+ - **Collecting into a hash:**
258
+ - The matched part is removed from each name, then modifiers are applied (only `:downcase` so far). Names left empty are skipped.
259
+ - The hash is decoded into the declared type, and variables that aren't attributes are ignored. Keys come out as the type expects them: symbols for `Data` structs, `Hash[name: …]` schemas and `Hash[Symbol, …]` maps, and strings for `Hash[String, …]` maps and string-keyed schemas.
260
+ - **The declared type must take a hash:** a `Hash` schema or map, or a `Data` struct, including nullable ones, ones with defaults, or unions with a hash branch. This is checked when `#env` is called, so `env(/^USER_/ => 'user.email')` with a `String` type raises `ArgumentError` right away, suggesting a single variable instead.
261
+ - **Modifiers only apply when collecting** with a regex, or all variables. Using one with a single variable raises `ArgumentError`, and so does an unknown modifier.
262
+ - **Prefer a regex to collecting everything.** ENV is shared by the whole process, so `env(:downcase, 'user.info')` would read a `user` or `home` attribute from the system's `USER` or `HOME`.
263
+ - **One call can map several sources,** ex. `env(:downcase, /^USER_/ => 'user.info', /^APP_/ => 'app.settings')`. Every source, key and type is checked before any component is implemented.
264
+ - **ENV is read on `#build!`,** not when `#env` is called.
265
+ - **Keys are relative to the component** `#env` is called on, like `#component!`. An app can implement a mounted library's components from ENV: `App.env('DB_URL' => 'my_lib.db.url')`.
266
+ - **Untyped components** (declared without a type, so `Any`) get raw strings: the variable's value, or a hash of them with string keys.
267
+ - **Missing or invalid variables fail the build** with `Sourced::Component::ENVProvider::Error` (a `Plumb::ParseError`) naming the component and each variable. Values are left out of the message, since ENV often holds secrets. Use a nullable type, an optional attribute or a default if a variable may be absent.
268
+
269
+ ```
270
+ invalid ENV for user.email: USER_EMAIL is missing
271
+
272
+ invalid ENV for user.info:
273
+ USER_DOB is invalid: Must match /\A\d{4}-\d{2}-\d{2}\z/
274
+ email is missing from ENV variables matching /^USER_/
275
+ ```
276
+
277
+ If a missing attribute matches a variable in a different case, the message suggests `:downcase`.
278
+
279
+ Under the hood, `#env` implements components with [`ENVProvider`](#providers), which can also be used directly, ex. for a dynamic component that reads ENV on every read: `App.component('flag', Sourced::Component::ENVProvider.new('FLAG'))`.
280
+
281
+ See [examples/env.rb](examples/env.rb).
282
+
283
+ ## Reading values
284
+
285
+ `#[]` reads a component's value by key, once the component is built.
286
+
287
+ ```ruby
288
+ App['db']
289
+ App['settings.retries']
290
+ App.node('db') # the node itself, a Sourced::Component
291
+ App.declared?('db') # => true
292
+ ```
293
+
294
+ Reads don't lock, and raise:
295
+
296
+ - `NotBuiltError` before the component is built
297
+ - `UndeclaredComponentError` for unknown keys, or for namespaces, which have no value
298
+
299
+ Values stay readable after teardown.
300
+
301
+ ## Lifecycle
302
+
303
+ The root component drives the lifecycle of the whole tree. Each step runs the matching hook of every component, in dependency order (dependencies first), and moves every component to a new status:
304
+
305
+ | Method | Does | Status |
306
+ | --- | --- | --- |
307
+ | `#prepare!` | Checks the tree (see below), sorts components by dependency, runs `prepare` hooks | `:prepared` |
308
+ | `#build!` | Builds singletons and parses their values through their types | `:built` |
309
+ | `#start!(context = Thread.current)` | Runs `start` hooks with `(value, context)` | `:started` |
310
+ | `#teardown!` | Runs `stop` hooks on started components, then `teardown` hooks, with `(value)`, in reverse order | `:torn_down` |
311
+
312
+ Each step runs the ones before it if needed (`#start!` prepares and builds), and is idempotent. `#teardown!` is a no-op unless the component is started. `:torn_down` is terminal: `#start!` on a torn down component raises `TornDownError`.
313
+
314
+ ```ruby
315
+ App.boot_status # => :started, the root's status
316
+ App.node('db').status # => :started, a component's status
317
+ App.ordered_nodes # components in dependency order, once prepared
318
+ ```
319
+
320
+ `#prepare!` raises:
321
+
322
+ - `UnimplementedComponentError` for declared components without an implementation, listing every one
323
+ - `MissingDependencyError` for dependencies that aren't declared, or that are namespaces without an implementation
324
+ - `CircularDependencyError` for dependency cycles
325
+
326
+ Once prepared, the tree is locked: declaring, implementing or mounting anything raises `LockedComponentError`.
327
+
328
+ ### Long-running components
329
+
330
+ `start` hooks run while holding the root's lock, so components that do long-running work (workers, servers, pollers) should spawn a thread or fiber and return. The context passed to `#start!` (the current thread by default) can be used to spawn work in a particular place, ex. an Async task.
331
+
332
+ ```ruby
333
+ App.declare('worker', Worker)
334
+ App.component!('worker', ['db']) do
335
+ build { |db| Worker.new(db) }
336
+ start { |worker, _context| worker.start } # spawns a thread and returns
337
+ teardown { |worker| worker.stop } # signals the thread, and joins it
338
+ end
339
+
340
+ App.start!
341
+ trap('TERM') { Thread.main.raise(Interrupt) }
342
+ begin
343
+ sleep # components run in their own threads
344
+ rescue Interrupt
345
+ ensure
346
+ App.teardown!
347
+ end
348
+ ```
349
+
350
+ Components that depend on others start after them and are torn down before them, so a producer that depends on a worker never pushes work to a stopped worker. See [examples/tree.rb](examples/tree.rb).
351
+
352
+ ### Deferred components, and starting and stopping by key
353
+
354
+ `#defer(key)` makes the root's `#start!` skip a component, along with every component that depends on it, directly or not, since they can't start before it. Start it by key instead, when it should run, and stop it by key when it shouldn't, any number of times, ex. workers that only run while their process holds a leader lock:
355
+
356
+ ```ruby
357
+ App.defer('sourced.dispatcher')
358
+ App.start!(task) # everything but the dispatcher, and whatever depends on it
359
+
360
+ elector.on_promote { App.start_component!('sourced.dispatcher', task) }
361
+ elector.on_demote { App.stop_component!('sourced.dispatcher') }
362
+ ```
363
+
364
+ Starting and stopping follow the dependency graph:
365
+
366
+ - **`#start_component!(key, context = Thread.current)`** starts any of the component's dependencies that aren't running, in dependency order, then the component, then the components depending on it that aren't running, once all their dependencies are. A no-op for a component already running.
367
+ - **`#stop_component!(key)`** stops every running component that depends on it, directly or not, in reverse dependency order, then the component. Its own dependencies keep running. Each one runs its `stop` hooks and keeps its value, so it can be started again: the value must support it.
368
+ - **`#restart_component!(key, context = Thread.current)`** stops, then starts.
369
+
370
+ ```
371
+ db <- store <- dispatcher <- monitor
372
+
373
+ App.stop_component!('dispatcher') # stops monitor, then dispatcher
374
+ App.start_component!('dispatcher') # starts dispatcher, then monitor
375
+ ```
376
+
377
+ - **A component stopped by key stays stopped** until it's started by key: starting one of its dependencies again doesn't start it, nor does the root. The components its stop stopped start again with it. A deferred component is the same, until its first start.
378
+ - **Statuses:** a deferred component is `:built` until it's started. A stopped one is `:stopped`. `#tree` and `#graph` show both, and which components are deferred.
379
+ - **Keys are relative to the component** the methods are called on, like `#component!`, and the root must be started, or starting: a `start` hook can start a deferred component, ex. right away when its process is already leader. Before that they raise `NotStartedError`, and `TornDownError` once the root is torn down.
380
+ - **Deferring belongs to the node,** not to its implementation, so implementing a deferred component again keeps it deferred. Any component can defer a node below it, like implementing one, ex. an app deferring a mounted library's component. Like declaring and implementing, it's only allowed before the tree is prepared.
381
+ - **If a `start` hook raises,** the components that call started are stopped again, in reverse order, and the error is re-raised. The rest of the tree is left as it was, and the root stays started.
382
+ - **If `stop` hooks raise,** every component is still stopped, and the first error is re-raised.
383
+ - **`#teardown!`** runs `stop` on the started components and `teardown` on all of them, deferred or stopped ones included.
384
+
385
+ ### Signal handlers
386
+
387
+ The lifecycle methods, and anything else that takes the root's lock (declaring, implementing and mounting components, `#graph`, `#tree`), can't be called from a `trap` block: Ruby doesn't allow locking a `Monitor` in trap context, so they raise `ThreadError: can't be called from trap context` and nothing is torn down. Reading values doesn't take the lock.
388
+
389
+ Instead, have the trap wake up the main thread, and tear down from there, as in the example above:
390
+
391
+ ```ruby
392
+ # Don't: raises ThreadError, and the components keep running
393
+ trap('TERM') { App.teardown! }
394
+
395
+ # Do: interrupt the main thread, and tear down in its ensure block
396
+ trap('TERM') { Thread.main.raise(Interrupt) }
397
+ ```
398
+
399
+ Any other way out of trap context works too, ex. pushing to a `Queue` or writing to a self-pipe that a thread waits on.
400
+
401
+ ### Errors while starting and tearing down
402
+
403
+ - If a `start` hook raises, the components already started are torn down in reverse order, the component is left `:torn_down`, and the error is re-raised.
404
+ - If `teardown` hooks raise, every component is still torn down, and the first error is re-raised.
405
+
406
+ ## Mounting components
407
+
408
+ A library can declare its components in its own root component, with default implementations:
409
+
410
+ ```ruby
411
+ module MyLib
412
+ def self.component
413
+ @component ||= Sourced::Component.new.tap do |s|
414
+ s.declare('logger', T::Interface[:info]) { Logger.new($stdout, progname: 'my_lib') }
415
+ s.declare('store', Store)
416
+ s.component!('store', ['logger']) { build { |logger| MemoryStore.new(logger:) } }
417
+ end
418
+ end
419
+ end
420
+ ```
421
+
422
+ An application mounts it under a namespace with `#mount(key, mountable)`, which attaches the library's root component as a branch of the app's tree. The app reads its components under the namespace, and can implement (or re-implement) them:
423
+
424
+ ```ruby
425
+ App.declare('db', DB) { DB.new }
426
+ App.mount('my_lib', MyLib.component)
427
+
428
+ # Override the library's store, with the app's db
429
+ App.component!('my_lib.store', ['db']) { build { |db| DBStore.new(db) } }
430
+
431
+ App.start!
432
+ App['my_lib.store'] # => #<DBStore ...>
433
+ MyLib.component['store'] # => the same object
434
+ ```
435
+
436
+ Mounted components aren't copied. The tree is made of the same node objects, so the library reads the app's overrides through its own keys, ex. from classes that only know about `MyLib.component`.
437
+
438
+ Keys can be nested (`App.mount('libs.my_lib', MyLib.component)`), components can mount other components, and a mounted component can keep declaring components: every ancestor indexes them.
439
+
440
+ ### Mountables
441
+
442
+ `#mount` takes anything that implements `#to_component`, returning a `Sourced::Component`. Components implement it, returning themselves, and a library can implement it so apps mount the library itself:
443
+
444
+ ```ruby
445
+ module MyLib
446
+ def self.to_component = component
447
+ end
448
+
449
+ App.mount('my_lib', MyLib.component)
450
+ ```
451
+
452
+ `#mount` raises `ArgumentError` for objects that don't respond to `#to_component`, or whose `#to_component` doesn't return a `Sourced::Component`.
453
+
454
+ `#component!` and `#component` mount components too: given anything that implements `#to_component`, they're an alias to `#mount`.
455
+
456
+ ```ruby
457
+ App.component('my_lib', MyLib) # same as App.mount('my_lib', MyLib)
458
+ ```
459
+
460
+ Mounting takes no provider or block, so passing either along with a component raises `ArgumentError`.
461
+
462
+ ### Dependencies are relative to the implementing component
463
+
464
+ Dependency keys are resolved from the component that called `#component!` (or `#component`):
465
+
466
+ - The library's `component!('store', ['logger'])` depends on `my_lib.logger`.
467
+ - The app's `component!('my_lib.store', ['db'])` depends on the app's `db`.
468
+
469
+ So a library's implementations can only depend on components in its own tree, and an application wires library components to its own components by overriding them.
470
+
471
+ ### Ownership
472
+
473
+ Components own their declarations and the sub-trees under them. Any component can implement any node below it, but can only declare under nodes it declared itself:
474
+
475
+ ```ruby
476
+ App.declare('my_lib.extra', String)
477
+ # => Sourced::Component::OwnershipError: my_lib is owned by my_lib: declare it there. This component can only implement it
478
+ ```
479
+
480
+ The root of the tree owns the lifecycle. Booting a mounted component directly (`MyLib.component.start!`) raises `SubcomponentError`: boot the root.
481
+
482
+ `#mount` raises if the component (what `#to_component` returns) is already mounted somewhere, is the root of the tree it's being mounted into, isn't open, or if the key is taken.
483
+
484
+ ### Extension points
485
+
486
+ Ownership is checked against the component `#declare` is called on, not against the code calling it. So an application can add a declaration to a library's tree by declaring it through the library's own component, ex. a reactor that the library collects with a [wildcard dependency](#wildcard-dependencies):
487
+
488
+ ```ruby
489
+ module MyLib
490
+ def self.component
491
+ @component ||= Sourced::Component.new.tap do |c|
492
+ c.declare('runner', Runner)
493
+ c.config!('runner', ['reactors.*']) { |reactors| Runner.new(reactors) }
494
+ end
495
+ end
496
+
497
+ # The one place where applications add to MyLib's tree
498
+ def self.reactor(name, type = Reactor) = component.declare("reactors.#{name}", type)
499
+ end
500
+
501
+ App.mount('my_lib', MyLib.component)
502
+ MyLib.reactor('emails') # declared in MyLib's tree, so reactors.* includes it
503
+ App.component!('my_lib.reactors.emails', ['mailer']) do # implemented by the app, with the app's deps
504
+ build { |mailer| EmailsReactor.new(mailer) }
505
+ end
506
+ ```
507
+
508
+ This should be the exception. A library owns its declarations because only the library knows what to do with them: it decides which components exist, what type they have, and what depends on them. An application declaring arbitrary keys in a library's tree is relying on the library's internals, and can break when they change. So:
509
+
510
+ - Only declare under namespaces that a library documents as extension points, like `reactors` above, and leave everything else to the library.
511
+ - Prefer a method that the library provides for it (`MyLib.reactor`) over calling `#declare` on its component directly. The library then decides the key and type, and can change how it stores them.
512
+ - Implement the declared components from the application, so they can depend on the application's components. Implementations given to the library's component resolve their dependencies from the library's tree.
513
+ - Declare before the root is prepared. Once it's prepared, the whole tree is locked, including mounted components.
514
+
515
+ ## Dependency injection
516
+
517
+ `#inject(*keys)` builds a module that injects components into a class, as keyword arguments to `#initialize` with readers. Each one defaults to the component's value, read when the object is instantiated.
518
+
519
+ ```ruby
520
+ class Dispatcher
521
+ include App.inject('logger', 'my_lib.store')
522
+
523
+ def dispatch(event)
524
+ store.append(event)
525
+ logger.info("dispatched #{event}")
526
+ end
527
+ end
528
+
529
+ Dispatcher.new.store # => App['my_lib.store']
530
+ Dispatcher.new(store: FakeStore.new) # any dependency can be passed explicitly, ex. in tests
531
+ ```
532
+
533
+ - Kwargs are named after the last segment of each key (`'my_lib.store'` => `store`). A hash gives them custom names: `App.inject('my_lib.store' => 'st')`.
534
+ - Keys are relative to the component `#inject` is called on: `App.node('my_lib').inject('store')`.
535
+ - Injections compose: a class can include several, and keep its own `#initialize` (positional and keyword arguments are passed through). Subclasses inherit them.
536
+ - Values are read on instantiation, so classes can be defined before the component is built, and dynamic components give each object a fresh value. Instantiating before the component is built raises `NotBuiltError`.
537
+ - Injecting an undeclared key raises `UndeclaredComponentError`, and injecting two components under the same name raises `ArgumentError`.
538
+ - Injections never overwrite methods: including one raises `InjectionError` if the class already has a method with an injected name, defined in the class or inherited, including private ones like `Kernel#format`. Inject under another name instead: `App.inject('logger' => 'app_logger')`. Methods defined after the include are the class' own, and replace the reader as usual.
539
+
540
+ Injectors hold on to the nodes themselves, so a library's classes can inject from the library's own root component, and get the overrides of the application that mounts it:
541
+
542
+ ```ruby
543
+ module MyLib
544
+ class Dispatcher
545
+ include MyLib.component.inject('store')
546
+ end
547
+ end
548
+
549
+ App.mount('my_lib', MyLib.component)
550
+ App.component!('my_lib.store', ['db']) { build { |db| DBStore.new(db) } }
551
+ App.start!
552
+
553
+ MyLib::Dispatcher.new.store # => #<DBStore ...>, the app's override
554
+ ```
555
+
556
+ ## Inspecting the tree
557
+
558
+ ```ruby
559
+ App.index.keys
560
+ # => ["db", "my_lib", "my_lib.logger", "my_lib.store"]
561
+
562
+ App.node('my_lib.store')
563
+ # => #<Sourced::Component my_lib.store Store (singleton, started)>
564
+
565
+ node = App.node('my_lib.store')
566
+ node.path # => "my_lib.store"
567
+ node.key # => "store"
568
+ node.parent # => the my_lib component
569
+ node.root # => App
570
+ node.owner # => the component that declared it
571
+ node.type # => the declared type
572
+ node.implementation # => deps, mode (:singleton, :dynamic or :alias) and the implementing component
573
+ node.children # => { segment => Component }
574
+ node.namespace? # => no type and no implementation
575
+ ```
576
+
577
+ ### `#tree`
578
+
579
+ Returns a `Sourced::Component::Tree` of the components under a component: how components are nested, which components are mounted, and who declared and implemented each one. (`#graph`, below, shows how components depend on each other instead.)
580
+
581
+ ```ruby
582
+ puts App.tree
583
+ ```
584
+
585
+ ```
586
+ (root)
587
+ ├── db DB (singleton, started)
588
+ ├── my_lib [mounted]
589
+ │ ├── logger Interface[info] (singleton, started)
590
+ │ └── store Store (singleton, started) implemented by (root)
591
+ └── cache
592
+ └── redis String (singleton, started)
593
+ └── pool Integer (not implemented, open)
594
+ ```
595
+
596
+ - **Mounted components** are marked `[mounted]`, and namespaces (no type, no implementation) are shown by their key alone.
597
+ - **`implemented by`** marks components implemented by a component other than the one that declared them, ex. an app overriding a library's component. `(root)` is the root of the tree.
598
+ - **Called on a mounted component,** it renders only the tree under it: `MyLib.component.tree`.
599
+
600
+ `tree.root` is a `Tree::Node`, with `#children`, and `tree.to_h` returns nested hashes:
601
+
602
+ ```ruby
603
+ tree = App.tree
604
+ tree.status # => :started, the root's status
605
+ tree.root.children.map(&:key) # => ["db", "my_lib", "cache"]
606
+
607
+ store = tree.root.children[1].children[1]
608
+ store.key # => "store"
609
+ store.path # => "my_lib.store"
610
+ store.type # => the declared type
611
+ store.type_name # => "Store"
612
+ store.namespace # => false
613
+ store.mounted # => false, true for mounted components
614
+ store.implemented # => true
615
+ store.mode # => :singleton
616
+ store.status # => :started
617
+ store.owner # => "my_lib", the full path of the component that declared it (nil for the root)
618
+ store.implementer # => nil, the full path of the component that implemented it (nil for the root)
619
+ store.overridden? # => true: implemented by a component other than its owner
620
+ store.children # => []
621
+ ```
622
+
623
+ `Tree#to_mermaid` returns a top-down [Mermaid](https://mermaid.js.org) flowchart of the tree:
624
+
625
+ ```ruby
626
+ puts App.tree.to_mermaid
627
+ ```
628
+
629
+ ```mermaid
630
+ flowchart TD
631
+ n0{{"(root)"}}:::component
632
+ n1["db<br/>DB<br/><i>singleton, started</i>"]:::started
633
+ n2{{"my_lib"}}:::component
634
+ n3["logger<br/>Interface[info]<br/><i>singleton, started</i>"]:::started
635
+ n4["store<br/>Store<br/><i>singleton, started</i><br/><i>implemented by (root)</i>"]:::started
636
+ n5("cache"):::namespace
637
+ n6["redis<br/>String<br/><i>singleton, started</i>"]:::started
638
+ n7["pool<br/>Integer<br/><i>not implemented</i>"]:::unimplemented
639
+ n0 --> n1
640
+ n0 --> n2
641
+ n2 --> n3
642
+ n2 --> n4
643
+ n0 --> n5
644
+ n5 --> n6
645
+ n6 --> n7
646
+ classDef started fill:#dcfce7,stroke:#16a34a
647
+ classDef unimplemented fill:#fef9c3,stroke:#ca8a04,stroke-dasharray:4 3
648
+ classDef namespace fill:#ffffff,stroke:#a1a1aa
649
+ classDef component fill:#fafafa,stroke:#18181b,stroke-width:2px
650
+ ```
651
+
652
+ - **Edges point from each node to its children.**
653
+ - **Components** (the root of the tree, and mounted components) are hexagons, and **namespaces** are rounded and plain.
654
+ - **Components** are drawn like in `Graph#to_mermaid`: singletons are rectangles, dynamic components are rounded, colored by status, and unimplemented ones are yellow and dashed. Components implemented by another component say which one.
655
+
656
+ ### `#graph`
657
+
658
+ Returns a `Sourced::Component::Graph` describing the components under a component, useful for tooling, visualisation or debugging. Components are listed by their full path from the root, in dependency order once the tree is prepared, and in declaration order before that. Namespaces without an implementation are left out.
659
+
660
+ ```ruby
661
+ graph = App.graph
662
+ graph.status # => :built, the root's status
663
+ graph.components # => an array of hashes, one per component
664
+
665
+ graph.to_h
666
+ # {
667
+ # status: :built,
668
+ # components: [
669
+ # {
670
+ # key: 'my_lib.store', # full path from the root
671
+ # type: <Plumb type>, # the declared type
672
+ # type_name: 'Interface[append]', # readable version of it
673
+ # implemented: true,
674
+ # mode: :singleton, # :singleton, :dynamic or :alias. nil if not implemented
675
+ # status: :built,
676
+ # deps: ['db'], # full paths of the components this one depends on
677
+ # missing: [], # deps that aren't declared, or are namespaces without an implementation
678
+ # dependents: ['app'], # components in the graph that depend on this one
679
+ # provider: <provider> # the provider, or the block of #config!/#config. nil for blocks of hooks
680
+ # },
681
+ # ...
682
+ # ]
683
+ # }
684
+ ```
685
+
686
+ It's a graph rather than a tree, since several components can share a dependency. Each component lists both `deps` and `dependents`, so you can walk the graph in either direction. Dependencies are resolved from the component that implemented each component, so an app's override of a library component lists the app's dependencies.
687
+
688
+ Calling `#graph` on a mounted component describes only the components under it, ex. `MyLib.component.graph` lists `my_lib.*`. Their `deps` can point outside it (to an app's components, through its overrides), and `dependents` only include components in that graph.
689
+
690
+ ### Mermaid diagrams
691
+
692
+ `Graph#to_mermaid` returns a [Mermaid](https://mermaid.js.org) flowchart of the dependency graph, which GitHub, many docs tools and editors render natively.
693
+
694
+ ```ruby
695
+ puts App.graph.to_mermaid
696
+ ```
697
+
698
+ ```mermaid
699
+ flowchart LR
700
+ c0["db<br/>DB<br/><i>singleton, started</i>"]:::started
701
+ c1["my_lib.logger<br/>Interface[info]<br/><i>singleton, started</i>"]:::started
702
+ c2["my_lib.store<br/>Interface[append]<br/><i>singleton, started</i>"]:::started
703
+ c3(["request_id<br/>String<br/><i>dynamic, started</i>"]):::started
704
+ c0 --> c2
705
+ classDef started fill:#dcfce7,stroke:#16a34a
706
+ ```
707
+
708
+ - **Arrows point from each dependency to the components that depend on it,** which is the order they're built and started in.
709
+ - **Each node shows** the full path, the declared type, and the mode and status.
710
+ - **Shapes:** singletons are rectangles and dynamic components are rounded.
711
+ - **Nodes are colored by status.** Declared but unimplemented components (yellow) and dependencies that aren't declared (red) have dashed borders, so problems that `#prepare!` would reject are visible in the diagram. In a mounted component's graph, dependencies outside it are drawn with a light dashed border.
712
+ - **Label text is escaped,** so type names with brackets, pipes or quotes are safe.
713
+
714
+ ## Events
715
+
716
+ Declaring, implementing and every lifecycle step publish an event to the root's notifier, ex. for telemetry:
717
+
718
+ ```ruby
719
+ App.notifier.subscribe('components.built') do |event|
720
+ Metrics.timing("boot.#{event.payload.key}", event.payload.duration)
721
+ end
722
+ ```
723
+
724
+ ### Event types
725
+
726
+ | Type | When | Payload |
727
+ | --- | --- | --- |
728
+ | `components.declared` | `#declare` | `key`, `type_name` |
729
+ | `components.implemented` | a component is implemented, or re-implemented | `key`, `mode`, `deps`, `implementer`, `override` |
730
+ | `components.preparing` / `components.prepared` | around a component's `prepare` hooks | `key`, and `duration` when finished |
731
+ | `components.building` / `components.built` | around a **singleton**'s `build` hooks | `key`, and `duration` when finished |
732
+ | `components.starting` / `components.started` | around a component's `start` hooks | `key`, and `duration` when finished |
733
+ | `components.stopping` / `components.stopped` | around a component's `stop` hooks, when stopped by key (on `#teardown!`, they're part of tearing down) | `key`, and `duration` when finished |
734
+ | `components.tearing_down` / `components.torn_down` | around a component's `teardown` hooks | `key`, and `duration` when finished |
735
+ | `components.deferred` | `#defer` | `key`, `deferrer` (the full path of the component that deferred it, `nil` for the root) |
736
+ | `components.failed` | a component's hook (or type check) raised | `key`, `stage`, `error_class`, `error_message`, `backtrace` |
737
+ | `root.preparing` / `root.prepared` | around `#prepare!` | `duration` when finished |
738
+ | `root.building` / `root.built` | around `#build!` | `duration` when finished |
739
+ | `root.starting` / `root.started` | around `#start!` | `duration` when finished |
740
+ | `root.tearing_down` / `root.torn_down` | around `#teardown!` | `duration` when finished |
741
+ | `root.failed` | a lifecycle step raised | `stage`, `error_class`, `error_message`, `backtrace` |
742
+
743
+ - **Every payload also has `pid`, `thread_id` and `fiber_id`:** the process, thread and fiber the event was published from (`Process.pid`, `Thread.current.object_id`, `Fiber.current.object_id`). Lifecycle events are published by whatever runs that step, so `components.started` shows where a component started.
744
+ - **`key`** is the component's full path from the root, ex. `sourced.db`.
745
+ - **`deps`** are relative to the `implementer`, the full path of the component that implemented the component (`nil` for the root).
746
+ - **`duration`** is in seconds, measured with a monotonic clock.
747
+ - **`stage`** is one of `:prepare`, `:build`, `:start`, `:stop` or `:teardown`.
748
+ - **Errors are described, not attached:** `error_class`, `error_message` and `backtrace` are strings, so events stay serializable (see below). The error itself is re-raised to the caller of the lifecycle method.
749
+
750
+ A few rules:
751
+
752
+ - **Build events are only published for singletons.** Dynamic components are built on every read, and publishing each one would be too noisy.
753
+ - **Events are only published for steps that run.** Repeat calls to `#prepare!`, `#build!` and friends are silent.
754
+ - **Failures are published before the error is re-raised.** A failed `#start!` publishes, in order: `components.failed` for the failing component, the teardown events from the rollback, then `root.failed`.
755
+
756
+ ```
757
+ root.starting
758
+ components.starting a → components.started a
759
+ components.starting b → components.failed b
760
+ components.tearing_down a → components.torn_down a
761
+ root.failed
762
+ ```
763
+
764
+ ### Event classes
765
+
766
+ Events are [`Sourced::Message`](https://github.com/ismasan/sourced-message) structs, all subclasses of `Sourced::Component::Event`:
767
+
768
+ ```
769
+ Sourced::Message
770
+ └── Sourced::Component::Event # payload: pid, thread_id, fiber_id
771
+ ├── Sourced::Component::Events::RootEvent # root.*
772
+ └── Sourced::Component::Events::ComponentEvent # components.*, adds key
773
+ ```
774
+
775
+ Each event type is a class, ex. `Sourced::Component::Events::ComponentBuilt`, with the usual message attributes (`id`, `type`, `created_at`, `metadata`, `payload`, ...). They're registered in `Sourced::Component::Event.registry`, which is also visible from `Sourced::Message.registry`:
776
+
777
+ ```ruby
778
+ Sourced::Component::Event.registry['components.built'] # => Sourced::Component::Events::ComponentBuilt
779
+ ```
780
+
781
+ So events can be serialized with Sourced::Message codecs, ex. to ship them to another process:
782
+
783
+ ```ruby
784
+ codec = Sourced::Message::JSONCodec.default.compile!
785
+ App.notifier.subscribe(Sourced::Component::Event) { |event| queue << JSON.dump(codec.encode(event)) }
786
+ ```
787
+
788
+ That's also why payloads only hold JSON-friendly values: `JSONCodec#compile!` checks every message type in the process, these events included.
789
+
790
+ ### The default notifier
791
+
792
+ `Sourced::Component.new` creates a `Sourced::Component::Notifier`. Every component in a tree publishes to its root's notifier, so `App.notifier` and `MyLib.component.notifier` are the same once `MyLib` is mounted. Events published by a component before it's mounted (ex. a library's declarations) go to its own notifier, so subscribe on the root before mounting and declaring, or before booting for lifecycle events.
793
+
794
+ ```ruby
795
+ # by type string (or symbol)
796
+ App.notifier.subscribe('root.started') { |event| ... }
797
+
798
+ # by class. Also matches subclasses
799
+ App.notifier.subscribe(Sourced::Component::Events::ComponentStarted) { |event| ... }
800
+ App.notifier.subscribe(Sourced::Component::Events::ComponentEvent) { |event| ... } # all component events
801
+ App.notifier.subscribe(Sourced::Component::Event) { |event| ... } # everything
802
+ ```
803
+
804
+ - **Unknown type strings raise `ArgumentError`,** so a typo can't silently subscribe to nothing.
805
+ - **Handlers run synchronously,** in the order they subscribed. They run in the thread or fiber performing the lifecycle step, while it holds the root's lock. Keep handlers fast, or hand the work off to a queue.
806
+ - **Errors raised by handlers propagate.** A handler that raises during `#start!` fails the boot and triggers the rollback.
807
+ - **Subscribing is thread safe,** and it can happen at any time, including after the component is locked.
808
+
809
+ ### Custom notifiers
810
+
811
+ Pass any object that responds to `#publish(event)` and `#subscribe(event_class_or_type, &block)` to the root:
812
+
813
+ ```ruby
814
+ class OTelNotifier
815
+ def publish(event) = Tracer.add_event(event.type, attributes: event.payload.to_h)
816
+ def subscribe(...) = raise(NotImplementedError)
817
+ end
818
+
819
+ App = Sourced::Component.new(notifier: OTelNotifier.new)
820
+ ```
821
+
822
+ The notifier is checked when the component is created. An object without these methods raises `Plumb::ParseError`.
823
+
824
+ ## Errors
825
+
826
+ All errors inherit from `Sourced::Component::ComponentError`, except type mismatches, which raise `Plumb::ParseError` naming the component (without the value, which can hold secrets):
827
+
828
+ ```
829
+ Plumb::ParseError: db.port: Must be a Integer
830
+ Plumb::ParseError: user: {age: "Must be a Integer"}
831
+ ```
832
+
833
+ | Error | Raised when |
834
+ | --- | --- |
835
+ | `DeclarationOverrideError` | declaring or mounting on a key that's already declared |
836
+ | `OwnershipError` | declaring or mounting under nodes owned by another component |
837
+ | `LockedComponentError` | changing the tree after it's prepared, or mounting a component that isn't open |
838
+ | `SubcomponentError` | booting a mounted component, or mounting a component that's already mounted |
839
+ | `UndeclaredComponentError` | implementing or reading an undeclared key, or reading a namespace |
840
+ | `UnimplementedComponentError` | preparing with declared (or deferred) components that have no implementation |
841
+ | `MissingDependencyError` | preparing with dependencies that aren't declared or implemented |
842
+ | `CircularDependencyError` | preparing with dependency cycles |
843
+ | `NotBuiltError` | reading values before the component is built |
844
+ | `TornDownError` | starting a component that's torn down, or starting or stopping a component by key once its root is torn down |
845
+ | `NotStartedError` | starting or stopping a component by key before its root is started |
846
+ | `InjectionError` | including an injector in a class that already has a method with an injected name, or already injects it |
847
+
848
+ ## Thread safety
849
+
850
+ Declaring, implementing, mounting and lifecycle methods are synchronized with a `Monitor` on the root of the tree (mounted components share their host's). A component can be booted from multiple threads: concurrent callers wait for the first one to finish, and then no-op. Reads (including injected defaults) take no lock, as values are immutable once built.
851
+
852
+ ## Development
853
+
854
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
855
+
856
+ Run the example with `bundle exec ruby examples/tree.rb` (Ctrl-C to stop).
857
+
858
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
859
+
860
+ ## License
861
+
862
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).