Module: Clacky::Channel::Adapters::UserAdapterLoader
- Defined in:
- lib/clacky/server/channel/user_adapter_loader.rb
Overview
Loads user-defined channel adapters from ~/.clacky/channels/<name>/adapter.rb.
Each adapter file is plain Ruby that defines a subclass of Clacky::Channel::Adapters::Base and self-registers via Adapters.register, exactly like the bundled adapters. This loader only discovers and requires those files after the built-in adapters are loaded — the existing self-registration mechanism then takes over with no further wiring.
A broken adapter (syntax error, missing interface methods) is isolated: it is skipped with a logged warning and never aborts the load of others.
Defined Under Namespace
Classes: Result
Constant Summary collapse
- DEFAULT_DIR =
File.("~/.clacky/channels")
- REQUIRED_CLASS_METHODS =
Required class/instance methods a user adapter must implement to be usable.
%i[platform_id platform_config].freeze
- REQUIRED_INSTANCE_METHODS =
%i[start stop send_text].freeze
Class Method Summary collapse
- .interface_gaps(klass) ⇒ Object
-
.last_result ⇒ Object
The result of the most recent load_all (set at startup).
-
.load_all(dir: DEFAULT_DIR) ⇒ Result
Names loaded and skipped (with reasons).
- .load_one(path, name, result) ⇒ Object
- .log_skip(name, reason) ⇒ Object
-
.scaffold(name, dir: DEFAULT_DIR) ⇒ String
Generate a ready-to-edit adapter skeleton at ~/.clacky/channels/<name>/adapter.rb.
- .skeleton(slug) ⇒ Object
- .unregister(klass) ⇒ Object
Class Method Details
.interface_gaps(klass) ⇒ Object
76 77 78 79 80 81 82 83 84 85 |
# File 'lib/clacky/server/channel/user_adapter_loader.rb', line 76 def self.interface_gaps(klass) missing = REQUIRED_CLASS_METHODS.reject { |m| klass.respond_to?(m) } # Base defines stub instance methods that only raise NotImplementedError, # so method_defined? alone passes via inheritance. Require the subclass to # actually override them — i.e. the method's owner must not be Base. missing += REQUIRED_INSTANCE_METHODS.reject do |m| klass.method_defined?(m) && klass.instance_method(m).owner != Base end missing end |
.last_result ⇒ Object
The result of the most recent load_all (set at startup). Lets ‘channel_verify` report status without re-requiring files (require is idempotent and would otherwise report already-loaded adapters as “did not register”).
44 45 46 |
# File 'lib/clacky/server/channel/user_adapter_loader.rb', line 44 def self.last_result @last_result || load_all end |
.load_all(dir: DEFAULT_DIR) ⇒ Result
Returns names loaded and skipped (with reasons).
29 30 31 32 33 34 35 36 37 38 39 |
# File 'lib/clacky/server/channel/user_adapter_loader.rb', line 29 def self.load_all(dir: DEFAULT_DIR) result = Result.new(loaded: [], skipped: []) if Dir.exist?(dir) Dir.glob(File.join(dir, "*", "adapter.rb")).sort.each do |path| name = File.basename(File.dirname(path)) load_one(path, name, result) end end @last_result = result result end |
.load_one(path, name, result) ⇒ Object
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/clacky/server/channel/user_adapter_loader.rb', line 48 def self.load_one(path, name, result) before = Adapters.all.dup require path newly = Adapters.all - before klass = newly.last unless klass result.skipped << [name, "did not register an adapter (missing Adapters.register?)"] log_skip(name, result.skipped.last[1]) return end if (missing = interface_gaps(klass)).any? unregister(klass) result.skipped << [name, "missing required methods: #{missing.join(", ")}"] log_skip(name, result.skipped.last[1]) return end result.loaded << name Clacky::Logger.info("[UserAdapterLoader] Loaded channel adapter '#{name}' → :#{klass.platform_id}") rescue StandardError, ScriptError => e result.skipped << [name, e.] log_skip(name, e.) end |
.log_skip(name, reason) ⇒ Object
94 95 96 |
# File 'lib/clacky/server/channel/user_adapter_loader.rb', line 94 def self.log_skip(name, reason) Clacky::Logger.warn("[UserAdapterLoader] Skipped channel adapter '#{name}': #{reason}") end |
.scaffold(name, dir: DEFAULT_DIR) ⇒ String
Generate a ready-to-edit adapter skeleton at ~/.clacky/channels/<name>/adapter.rb. The skeleton already self-registers and implements the full interface with TODO markers — the author only fills in the method bodies.
102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/clacky/server/channel/user_adapter_loader.rb', line 102 def self.scaffold(name, dir: DEFAULT_DIR) slug = name.to_s.strip.downcase.gsub(/[^a-z0-9_]+/, "_").gsub(/\A_+|_+\z/, "") raise ArgumentError, "invalid channel name: #{name.inspect}" if slug.empty? target_dir = File.join(dir, slug) path = File.join(target_dir, "adapter.rb") raise ArgumentError, "adapter already exists: #{path}" if File.exist?(path) FileUtils.mkdir_p(target_dir) File.write(path, skeleton(slug)) path end |
.skeleton(slug) ⇒ Object
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
# File 'lib/clacky/server/channel/user_adapter_loader.rb', line 115 def self.skeleton(slug) const = slug.split("_").map(&:capitalize).join <<~RUBY # frozen_string_literal: true # User-defined channel adapter for ":#{slug}". # Edit the TODO sections, then it loads automatically on next start. # Verify with: clacky channel verify module Clacky module Channel module Adapters class #{const}Adapter < Base def self.platform_id :#{slug} end # Map raw config (channels.yml `#{slug}` section) to a symbol-keyed hash. def self.platform_config(data) { # TODO: pull your credentials out of `data` # token: data["IM_#{slug.upcase}_TOKEN"] || data["token"] }.compact end def initialize(config) @config = config end # Begin receiving messages. Blocks until #stop — runs inside a Thread. # Yield one standardized event Hash per inbound message. def start(&on_message) # TODO: connect to your platform and loop, calling on_message.call(event) raise NotImplementedError end def stop # TODO: close connections / stop the read loop end # Send a plain text (or Markdown) message to a chat. # @return [Hash] { message_id: String } def send_text(chat_id, text, reply_to: nil) # TODO: call your platform's send API raise NotImplementedError end # Optional: validate config; return array of error strings (empty = ok). def validate_config(config) [] end Adapters.register(platform_id, self) end end end end RUBY end |
.unregister(klass) ⇒ Object
87 88 89 90 91 92 |
# File 'lib/clacky/server/channel/user_adapter_loader.rb', line 87 def self.unregister(klass) platform = (klass.platform_id if klass.respond_to?(:platform_id)) return unless platform Adapters.unregister(platform) end |