diff --git a/modules/aca/rooms/base.rb b/modules/aca/rooms/base.rb new file mode 100644 index 00000000..0c7b6333 --- /dev/null +++ b/modules/aca/rooms/base.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +load File.join(__dir__, 'component_manager.rb') + +module Aca; end +module Aca::Rooms; end + +class Aca::Rooms::Base + include ::Orchestrator::Constants + include ::Aca::Rooms::ComponentManager + + generic_name :System + implements :logic + + def self.setting(hash) + previous = @default_settings || {} + default_settings previous.merge hash + end + + # ------------------------------ + # Callbacks + + def on_load + on_update + end + + def on_update + self[:name] = system.name + self[:type] = self.class.name.demodulize + + @config = Hash.new do |h, k| + h[k] = setting(k) || default_setting[k] + end + end + + protected + + def default_setting + self.class.instance_variable_get :@default_settings + end + + attr_reader :config +end diff --git a/modules/aca/rooms/collab.rb b/modules/aca/rooms/collab.rb new file mode 100644 index 00000000..bdf49d2a --- /dev/null +++ b/modules/aca/rooms/collab.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +::Orchestrator::DependencyManager.load('Aca::Rooms::Base', :logic) + +class Aca::Rooms::Collab < Aca::Rooms::Base + descriptive_name 'ACA Collaboration Space' + description <<~DESC + Logic and external control API for collaboration spaces. + + Collaboration spaces are rooms / systems where the design is centered + around a VC system, with the primary purpose of collaborating with both + people in room, as well as remote parties. + DESC + + components :Power, :Io + + def on_update + super + end +end diff --git a/modules/aca/rooms/component_manager.rb b/modules/aca/rooms/component_manager.rb new file mode 100644 index 00000000..0e0f2bb3 --- /dev/null +++ b/modules/aca/rooms/component_manager.rb @@ -0,0 +1,152 @@ +# frozen_string_literal: true + +module Aca; end +module Aca::Rooms; end + +module Aca::Rooms::Components; end + +module Aca::Rooms::ComponentManager + module Composer + # Mini-DSL for defining cross-component behaviours. + # + # With the block provided `before`, `during`, or `after` can be used + # to insert additional behaviour to methods that should only exist when + # both components are in use. The result of the original method will + # be preserved, but will be deferred until the completion of any + # composite behaviours. + def compose_with(component, &extensions) + overlay = overlay_module_for component + + hooks = {} + + # Eval the block to populate hooks with the overlay actions as + # method => { position => [actions] } + composer = Class.new do + [:before, :during, :after].each do |position| + define_method position do |method, &action| + ((hooks[method] ||= {})[position] ||= []) << action + end + end + end + + composer.new.instance_eval(&extensions) + + # Build the overlay Module for prepending + hooks.each do |method, actions| + # FIXME: this removes visibility of original args + overlay.send :define_method, method do |*args| + result = nil + + sequence = [ + actions[:before], + [ + proc { |*x| result = super(*x) }, + *actions[:during] + ], + actions[:after] + ].compact! + + exec_actions = sequence.reduce(thread.finally) do |a, b| + a.then do + thread.all(b.map { |x| instance_exec(*args, &x) }) + end + end + + exec_actions.then { result } + end + end + + self + end + + private + + # Build out a module heirachy so that ::Compositions:: + # exists and can be used to house any behaviour extensions to be + # applied when both components are in use. + def overlay_module_for(component) + [:Compositions, component].reduce(self) do |context, name| + if context.const_defined? name, false + context.const_get name + else + context.const_set name, Module.new + end + end + end + end + + module Mixin + def components(*components) + component_settings = {} + + # Load the associated module + modules = components.map do |component| + mod, settings = load component + + component_settings.merge! settings do |key, _, _| + raise "setting \"#{key}\" declared in multiple components" + end + + mod + end + + # Include the components + include(*modules) + + # Compose cross-component behaviours + overlays = modules.flat_map do |base| + next unless base.const_defined? :Compositions, false + + components.each_with_object([]) do |component, compositions| + next unless base::Compositions.const_defined? component + compositions << base::Compositions.const_get(component) + end + end + prepend(*overlays.compact) + + # Bubble up settings definitions + setting component_settings + end + + private + + # Load a component, returning a reference to the Module and the + # settings it defines. + # + # Settings may be defined by using the 'setting' keyword within the + # component module. Support for this is temporarily mixed in during + # load below. + def load(component) + fqn = "::Aca::Rooms::Components::#{component}" + + settings = {} + + mod = if ::Aca::Rooms::Components.const_defined? component + ::Aca::Rooms::Components.const_get component + else + ::Aca::Rooms::Components.const_set component, Module.new + end + + # Inject a `setting` class method that pushes back into settings + # here via a closure. This enables any settings used by the + # component to be declared as `setting key: `. + mod.define_singleton_method :setting do |hash| + settings.merge! hash do |key, _, _| + raise "\"#{key}\" declared multiple times in #{fqn}" + end + end + + mod.extend Composer + + ::Orchestrator::DependencyManager.load fqn, :logic + + [mod, settings] + end + end + + module_function + + def included(other) + other.extend Mixin + end +end diff --git a/modules/aca/rooms/components/io.rb b/modules/aca/rooms/components/io.rb new file mode 100644 index 00000000..10126df8 --- /dev/null +++ b/modules/aca/rooms/components/io.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Aca::Rooms::Components::Io + setting default_routes: {} + + def show(source, on: default_outputs) + source = source.to_sym + target = Array(on).map(&:to_sym) + + logger.debug "Showing #{source} on #{target.join ','}" + + connect source => target + end + + protected + + def connect(signal_map) + logger.debug 'called connect' + end + + def blank(outputs) + logger.debug 'called blank' + end + + def default_outputs + [] + end +end + +# Aca::Rooms::Components::Io.extend ::Aca::Rooms::ComponentManager::Composer + +Aca::Rooms::Components::Io.compose_with :Power do + during :powerup do + connect {} # config.default_routes + end + + during :shutdown do + connect {} + end +end diff --git a/modules/aca/rooms/components/power.rb b/modules/aca/rooms/components/power.rb new file mode 100644 index 00000000..c1e9c5f3 --- /dev/null +++ b/modules/aca/rooms/components/power.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Aca::Rooms::Components::Power + setting powerup_actions: {} + + setting shutdown_actions: {} + + def powerup + logger.debug 'in power mod' + :online + end + + def shutdown + :shutdown + end +end