From 85f8cd019dd275fb3ed0f4c0197d3a0c2d719e99 Mon Sep 17 00:00:00 2001 From: Kim Burgess Date: Wed, 24 Oct 2018 23:53:58 +1000 Subject: [PATCH 1/8] (aca:rooms) initial component framework --- modules/aca/rooms/base.rb | 23 +++++++ modules/aca/rooms/collab.rb | 26 ++++++++ modules/aca/rooms/component_manager.rb | 87 ++++++++++++++++++++++++++ modules/aca/rooms/components/io.rb | 38 +++++++++++ modules/aca/rooms/components/power.rb | 11 ++++ 5 files changed, 185 insertions(+) create mode 100644 modules/aca/rooms/base.rb create mode 100644 modules/aca/rooms/collab.rb create mode 100644 modules/aca/rooms/component_manager.rb create mode 100644 modules/aca/rooms/components/io.rb create mode 100644 modules/aca/rooms/components/power.rb diff --git a/modules/aca/rooms/base.rb b/modules/aca/rooms/base.rb new file mode 100644 index 00000000..1661dd22 --- /dev/null +++ b/modules/aca/rooms/base.rb @@ -0,0 +1,23 @@ +# 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 + + # ------------------------------ + # Callbacks + + def on_load + on_update + end + + def on_update + self[:name] = system.name + self[:type] = self.class.name.demodulize + end +end diff --git a/modules/aca/rooms/collab.rb b/modules/aca/rooms/collab.rb new file mode 100644 index 00000000..32ec1b5d --- /dev/null +++ b/modules/aca/rooms/collab.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'ostruct' + +::Orchestrator::DependencyManager.load('Aca::Rooms::Base', :logic) + +class Aca::Rooms::Collab < Aca::Rooms::Base + descriptive_name 'ACA Collaboration Space' + generic_name :System + implements :logic + 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 + logger.debug "Methods are: #{self.methods}" + logger.debug ::Aca::Rooms::Base.new.methods + end +end diff --git a/modules/aca/rooms/component_manager.rb b/modules/aca/rooms/component_manager.rb new file mode 100644 index 00000000..7b8a6365 --- /dev/null +++ b/modules/aca/rooms/component_manager.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +module Aca; end +module Aca::Rooms; end + +module Aca::Rooms::Components; end + +module Aca::Rooms::ComponentManager + module Composer + Hook = Struct.new :before, :during, :after + + def compose_with(component, &extensions) + # Nested hash of { components => { method => Hook } } + const_set :HOOKS, {} unless const_defined? :HOOKS + hooks = const_get :HOOKS + hooks[component] ||= Hash.new { |h, k| h[k] = Hook.new } + + # Mini-DSL for defining cross-component behaviours + composer = Class.new do + Hook.members.each do |position| + define_method position do |method, &action| + hooks[component][method][position] = action + end + end + end + + composer.new.instance_eval(&extensions) + + self + + # HOOKS = { + # Power: { + # powerup: ... + # } + # } + # + # HOOKS = { + # powerup: [...] + # } + end + end + + module Mixin + def components(*components) + modules = components.map do |component| + fqn = "::Aca::Rooms::Components::#{component}" + ::Orchestrator::DependencyManager.load fqn, :logic + end + + # Include the component methods + include(*modules) + + # Get all cross-component behaviours for the loaded components + hooks = modules.select { |mod| mod.singleton_class < Composer } + .flat_map { |mod| mod::HOOKS.values_at(*components) } + .compact + + # Map [{ method => Hook }] to { method => [Hook] } + hooks = hooks.reduce { |a, b| a.merge(b) { |_, x, y| [*x, *y] } } + .transform_values! { |x| Array.wrap x } + + overrides = Module.new do + hooks.each do |method, hook_list| + define_method method do |*args| + actions = [ + [hook_list.map(&:before)], + [hook_list.map(&:during), -> { super }], + [hook_list.map(&:after)] + ] + + actions.reduce do + + end + end + end + end + + prepend overrides + 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..0dbca74c --- /dev/null +++ b/modules/aca/rooms/components/io.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Aca::Rooms::Components::Io + 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..49d63afe --- /dev/null +++ b/modules/aca/rooms/components/power.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Aca::Rooms::Components::Power + def powerup + + end + + def shutdown + + end +end From 73a87d2d9abfb65ea0da4e6c0874c2d3455f7abd Mon Sep 17 00:00:00 2001 From: Kim Burgess Date: Tue, 20 Nov 2018 17:50:17 +1000 Subject: [PATCH 2/8] (aca:rooms) refactor component composition --- modules/aca/rooms/component_manager.rb | 56 +++++++++++++------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/modules/aca/rooms/component_manager.rb b/modules/aca/rooms/component_manager.rb index 7b8a6365..61ca8da7 100644 --- a/modules/aca/rooms/component_manager.rb +++ b/modules/aca/rooms/component_manager.rb @@ -7,19 +7,18 @@ module Aca::Rooms::Components; end module Aca::Rooms::ComponentManager module Composer - Hook = Struct.new :before, :during, :after + Hook = Struct.new :method, :position, :action def compose_with(component, &extensions) - # Nested hash of { components => { method => Hook } } + # Hash of { component => [Hook] } const_set :HOOKS, {} unless const_defined? :HOOKS - hooks = const_get :HOOKS - hooks[component] ||= Hash.new { |h, k| h[k] = Hook.new } + hooks = const_get(:HOOKS)[component] = [] # Mini-DSL for defining cross-component behaviours composer = Class.new do - Hook.members.each do |position| + [:before, :during, :after].each do |position| define_method position do |method, &action| - hooks[component][method][position] = action + hooks << Hook.new(method, position, action) end end end @@ -27,16 +26,6 @@ def compose_with(component, &extensions) composer.new.instance_eval(&extensions) self - - # HOOKS = { - # Power: { - # powerup: ... - # } - # } - # - # HOOKS = { - # powerup: [...] - # } end end @@ -54,22 +43,33 @@ def components(*components) hooks = modules.select { |mod| mod.singleton_class < Composer } .flat_map { |mod| mod::HOOKS.values_at(*components) } .compact + .flatten - # Map [{ method => Hook }] to { method => [Hook] } - hooks = hooks.reduce { |a, b| a.merge(b) { |_, x, y| [*x, *y] } } - .transform_values! { |x| Array.wrap x } + # Map from [Hook] to { method => { position => [action] } } + hooks = hooks.each_with_object({}) do |hook, h| + ((h[hook.method] ||= {})[hook.position] ||= []) << hook.action + end overrides = Module.new do - hooks.each do |method, hook_list| + hooks.each do |method, actions| define_method method do |*args| - actions = [ - [hook_list.map(&:before)], - [hook_list.map(&:during), -> { super }], - [hook_list.map(&:after)] - ] - - actions.reduce do - + result = nil + + sequence = [ + actions[:before], + [ + proc { result = super(*args) }, + *actions[:during] + ], + actions[:after] + ].compact! + + sequence.reduce(thread.finally) do |prev, succ| + prev.then do + thread.all(succ.map { |x| instance_exec(*args, &x) }) + end + end.then do + result end end end From d186add4480bd919adcfe9e2c17bba644cc8e71b Mon Sep 17 00:00:00 2001 From: Kim Burgess Date: Mon, 26 Nov 2018 01:15:22 +1000 Subject: [PATCH 3/8] (aca:rooms) fix component compositions stacking with every reload --- modules/aca/rooms/component_manager.rb | 98 +++++++++++++++----------- 1 file changed, 56 insertions(+), 42 deletions(-) diff --git a/modules/aca/rooms/component_manager.rb b/modules/aca/rooms/component_manager.rb index 61ca8da7..5dbbd30d 100644 --- a/modules/aca/rooms/component_manager.rb +++ b/modules/aca/rooms/component_manager.rb @@ -7,75 +7,89 @@ module Aca::Rooms::Components; end module Aca::Rooms::ComponentManager module Composer - Hook = Struct.new :method, :position, :action - + # 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) - # Hash of { component => [Hook] } - const_set :HOOKS, {} unless const_defined? :HOOKS - hooks = const_get(:HOOKS)[component] = [] + # 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. + overlay = [: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 + + hooks = {} - # Mini-DSL for defining cross-component behaviours + # 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 << Hook.new(method, position, 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! + + sequence.reduce(thread.finally) do |a, b| + a.then do + thread.all(b.map { |x| instance_exec(*args, &x) }) + end + end.then do + result + end + end + end + self end end module Mixin def components(*components) + # Load the associated module modules = components.map do |component| fqn = "::Aca::Rooms::Components::#{component}" ::Orchestrator::DependencyManager.load fqn, :logic end - # Include the component methods + # Include the component include(*modules) - # Get all cross-component behaviours for the loaded components - hooks = modules.select { |mod| mod.singleton_class < Composer } - .flat_map { |mod| mod::HOOKS.values_at(*components) } - .compact - .flatten - - # Map from [Hook] to { method => { position => [action] } } - hooks = hooks.each_with_object({}) do |hook, h| - ((h[hook.method] ||= {})[hook.position] ||= []) << hook.action - end + # Compose cross-component behaviours + overlays = modules.flat_map do |base| + next unless base.const_defined? :Compositions, false - overrides = Module.new do - hooks.each do |method, actions| - define_method method do |*args| - result = nil - - sequence = [ - actions[:before], - [ - proc { result = super(*args) }, - *actions[:during] - ], - actions[:after] - ].compact! - - sequence.reduce(thread.finally) do |prev, succ| - prev.then do - thread.all(succ.map { |x| instance_exec(*args, &x) }) - end - end.then do - result - end - end + components.each_with_object([]) do |component, compositions| + next unless base::Compositions.const_defined? component + compositions << base::Compositions.const_get(component) end end - - prepend overrides + prepend(*overlays.compact) end end From dbfb426b267bcb6826c52e3d78c3785f666f8132 Mon Sep 17 00:00:00 2001 From: Kim Burgess Date: Mon, 26 Nov 2018 01:41:30 +1000 Subject: [PATCH 4/8] (aca:rooms) move construction of overlay modules to its own method --- modules/aca/rooms/component_manager.rb | 32 +++++++++++++++----------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/modules/aca/rooms/component_manager.rb b/modules/aca/rooms/component_manager.rb index 5dbbd30d..a7234eb2 100644 --- a/modules/aca/rooms/component_manager.rb +++ b/modules/aca/rooms/component_manager.rb @@ -15,16 +15,7 @@ module Composer # be preserved, but will be deferred until the completion of any # composite behaviours. def compose_with(component, &extensions) - # 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. - overlay = [: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 + overlay = overlay_module_for component hooks = {} @@ -55,18 +46,33 @@ def compose_with(component, &extensions) actions[:after] ].compact! - sequence.reduce(thread.finally) do |a, b| + exec_actions = sequence.reduce(thread.finally) do |a, b| a.then do thread.all(b.map { |x| instance_exec(*args, &x) }) end - end.then do - result 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 From 7b99ad5594a39a435e746e085519fec1d1d0a1ce Mon Sep 17 00:00:00 2001 From: Kim Burgess Date: Fri, 30 Nov 2018 00:20:24 +1000 Subject: [PATCH 5/8] (aca:rooms) add support for defining component settings --- modules/aca/rooms/base.rb | 17 ++++++++++ modules/aca/rooms/component_manager.rb | 47 ++++++++++++++++++++++++-- modules/aca/rooms/components/io.rb | 2 ++ modules/aca/rooms/components/power.rb | 9 +++-- 4 files changed, 70 insertions(+), 5 deletions(-) diff --git a/modules/aca/rooms/base.rb b/modules/aca/rooms/base.rb index 1661dd22..70b2303c 100644 --- a/modules/aca/rooms/base.rb +++ b/modules/aca/rooms/base.rb @@ -9,6 +9,11 @@ class Aca::Rooms::Base include ::Orchestrator::Constants include ::Aca::Rooms::ComponentManager + def self.setting(hash) + previous = @default_settings || {} + default_settings previous.merge hash + end + # ------------------------------ # Callbacks @@ -19,5 +24,17 @@ def on_load 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/component_manager.rb b/modules/aca/rooms/component_manager.rb index a7234eb2..3c7f2b5d 100644 --- a/modules/aca/rooms/component_manager.rb +++ b/modules/aca/rooms/component_manager.rb @@ -77,13 +77,20 @@ def overlay_module_for(component) module Mixin def components(*components) + component_settings = {} + # Load the associated module modules = components.map do |component| - fqn = "::Aca::Rooms::Components::#{component}" - ::Orchestrator::DependencyManager.load fqn, :logic + mod, settings = load component + + component_settings.merge! settings do |key, _, _| + raise "setting \"#{key}\" declared in multiple components" + end + + mod end - # Include the component + # Include the components include(*modules) # Compose cross-component behaviours @@ -96,6 +103,40 @@ def components(*components) 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 + + mod.define_singleton_method :setting do |hash| + settings.merge! hash do |key, _, _| + raise "\"#{key}\" declared multiple times in #{fqn}" + end + end + + + ::Orchestrator::DependencyManager.load fqn, :logic + + [mod, settings] end end diff --git a/modules/aca/rooms/components/io.rb b/modules/aca/rooms/components/io.rb index 0dbca74c..8c7cb19a 100644 --- a/modules/aca/rooms/components/io.rb +++ b/modules/aca/rooms/components/io.rb @@ -1,6 +1,8 @@ # 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) diff --git a/modules/aca/rooms/components/power.rb b/modules/aca/rooms/components/power.rb index 49d63afe..c1e9c5f3 100644 --- a/modules/aca/rooms/components/power.rb +++ b/modules/aca/rooms/components/power.rb @@ -1,11 +1,16 @@ # frozen_string_literal: true module Aca::Rooms::Components::Power - def powerup + setting powerup_actions: {} + + setting shutdown_actions: {} + def powerup + logger.debug 'in power mod' + :online end def shutdown - + :shutdown end end From f6ceabcaf5fcf81b6cee238040a4d8af5ca87de9 Mon Sep 17 00:00:00 2001 From: Kim Burgess Date: Fri, 30 Nov 2018 18:25:46 +1000 Subject: [PATCH 6/8] (aca:rooms) move generic name and module type def into base class --- modules/aca/rooms/base.rb | 3 +++ modules/aca/rooms/collab.rb | 6 ------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/modules/aca/rooms/base.rb b/modules/aca/rooms/base.rb index 70b2303c..0c7b6333 100644 --- a/modules/aca/rooms/base.rb +++ b/modules/aca/rooms/base.rb @@ -9,6 +9,9 @@ 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 diff --git a/modules/aca/rooms/collab.rb b/modules/aca/rooms/collab.rb index 32ec1b5d..bdf49d2a 100644 --- a/modules/aca/rooms/collab.rb +++ b/modules/aca/rooms/collab.rb @@ -1,13 +1,9 @@ # frozen_string_literal: true -require 'ostruct' - ::Orchestrator::DependencyManager.load('Aca::Rooms::Base', :logic) class Aca::Rooms::Collab < Aca::Rooms::Base descriptive_name 'ACA Collaboration Space' - generic_name :System - implements :logic description <<~DESC Logic and external control API for collaboration spaces. @@ -20,7 +16,5 @@ class Aca::Rooms::Collab < Aca::Rooms::Base def on_update super - logger.debug "Methods are: #{self.methods}" - logger.debug ::Aca::Rooms::Base.new.methods end end From 98752523f6e211fdf4d512fcee83ddb02daaa784 Mon Sep 17 00:00:00 2001 From: Kim Burgess Date: Fri, 30 Nov 2018 18:26:47 +1000 Subject: [PATCH 7/8] (aca:rooms) auto extend composer from component loader --- modules/aca/rooms/component_manager.rb | 1 + modules/aca/rooms/components/io.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/aca/rooms/component_manager.rb b/modules/aca/rooms/component_manager.rb index 3c7f2b5d..84f48a6b 100644 --- a/modules/aca/rooms/component_manager.rb +++ b/modules/aca/rooms/component_manager.rb @@ -133,6 +133,7 @@ def load(component) end end + mod.extend Composer ::Orchestrator::DependencyManager.load fqn, :logic diff --git a/modules/aca/rooms/components/io.rb b/modules/aca/rooms/components/io.rb index 8c7cb19a..10126df8 100644 --- a/modules/aca/rooms/components/io.rb +++ b/modules/aca/rooms/components/io.rb @@ -27,7 +27,7 @@ def default_outputs end end -Aca::Rooms::Components::Io.extend ::Aca::Rooms::ComponentManager::Composer +# Aca::Rooms::Components::Io.extend ::Aca::Rooms::ComponentManager::Composer Aca::Rooms::Components::Io.compose_with :Power do during :powerup do From ac5a69bda3359531ba51ef0f2a006ac0bcabb8f0 Mon Sep 17 00:00:00 2001 From: Kim Burgess Date: Fri, 30 Nov 2018 18:31:42 +1000 Subject: [PATCH 8/8] (aca:rooms) add docs --- modules/aca/rooms/component_manager.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/aca/rooms/component_manager.rb b/modules/aca/rooms/component_manager.rb index 84f48a6b..0e0f2bb3 100644 --- a/modules/aca/rooms/component_manager.rb +++ b/modules/aca/rooms/component_manager.rb @@ -127,6 +127,9 @@ def load(component) ::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}"