refactor: align Ruby host with RuboCop while retaining double quotes
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Ruby DSL and protocol callbacks share the host's singleton state.
|
||||
module Vamp
|
||||
# Checks protocol envelopes and normalizes document snapshots before callbacks.
|
||||
module FrameValidation
|
||||
# @param startup [Object] action catalog and options
|
||||
# @return [void]
|
||||
def self.startup!(startup)
|
||||
return if hash_shape?(startup, %w[core_actions options]) && startup["options"].is_a?(Hash)
|
||||
|
||||
raise TypeError, "invalid Ruby startup catalog/options"
|
||||
end
|
||||
|
||||
# @param request [Object] invocation frame
|
||||
# @return [void]
|
||||
def self.invocation!(request)
|
||||
return if envelope?(request, "invoke", %w[args command context id type version]) &&
|
||||
integer?(request["id"], 1)
|
||||
|
||||
raise ArgumentError, "invalid invocation frame"
|
||||
end
|
||||
|
||||
# @param request [Object] document event frame
|
||||
# @return [void]
|
||||
def self.document!(request)
|
||||
return if envelope?(request, "document", %w[document event type version]) &&
|
||||
%w[open change save close].include?(request["event"])
|
||||
|
||||
raise ArgumentError, "invalid document event frame"
|
||||
end
|
||||
|
||||
# @param document [Object] document snapshot normalized in place
|
||||
# @return [Hash{String => String, Integer, nil}]
|
||||
def self.snapshot!(document)
|
||||
unless hash_shape?(document, %w[id path revision root text]) &&
|
||||
integer?(document["id"], 0) && integer?(document["revision"], 0)
|
||||
raise ArgumentError, "invalid document snapshot"
|
||||
end
|
||||
|
||||
normalize_snapshot_strings(document)
|
||||
document
|
||||
end
|
||||
|
||||
# @param effects [Object] asynchronous effects
|
||||
# @return [void]
|
||||
def self.effects!(effects)
|
||||
return if effects.is_a?(Array) && effects.all?(Hash)
|
||||
|
||||
raise TypeError, "published effects must be an Array of Hashes"
|
||||
end
|
||||
|
||||
# @param request_id [Object] update's optional invocation ID
|
||||
# @return [void]
|
||||
def self.request_id!(request_id)
|
||||
return if request_id.nil? || (integer?(request_id, 1) && request_id <= 18_446_744_073_709_551_615)
|
||||
|
||||
raise ArgumentError, "request_id must be a positive unsigned 64-bit integer or nil"
|
||||
end
|
||||
|
||||
class << self
|
||||
private
|
||||
|
||||
# @param document [Hash{String => String, Integer, nil}]
|
||||
# @return [String, nil]
|
||||
def normalize_snapshot_strings(document)
|
||||
document["text"] = Vamp.string!(document["text"], "document text")
|
||||
document["root"] = Vamp.string!(document["root"], "document root")
|
||||
document["path"] = Vamp.string!(document["path"], "document path") unless document["path"].nil?
|
||||
end
|
||||
|
||||
# @param value [Object]
|
||||
# @param keys [Array<String>] sorted required keys
|
||||
# @return [Boolean]
|
||||
def hash_shape?(value, keys)
|
||||
value.is_a?(Hash) && value.keys.sort == keys
|
||||
end
|
||||
|
||||
# @param value [Object]
|
||||
# @param minimum [Integer] inclusive lower bound
|
||||
# @return [Boolean]
|
||||
def integer?(value, minimum)
|
||||
value.is_a?(Integer) && value >= minimum
|
||||
end
|
||||
|
||||
# @param request [Object]
|
||||
# @param type [String] expected frame kind
|
||||
# @param keys [Array<String>] sorted required keys
|
||||
# @return [Boolean]
|
||||
def envelope?(request, type, keys)
|
||||
hash_shape?(request, keys) && request["version"].is_a?(Integer) &&
|
||||
request["version"] == 1 && request["type"] == type
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# @param mode [String] config or plugin
|
||||
# @param startup [Hash{String => Object}] action catalog and plugin options
|
||||
# @param sender [Proc, Method] writes a Hash{String => Object} protocol frame
|
||||
# @return [Proc, Method] the installed sender
|
||||
def self.setup(mode, startup, sender)
|
||||
FrameValidation.startup!(startup)
|
||||
install_actions(startup["core_actions"])
|
||||
@mode = mode
|
||||
reset_registrations
|
||||
@options = deep_freeze(json_value!(startup["options"]))
|
||||
@sender = sender
|
||||
end
|
||||
|
||||
# @return [Hash{String => Object}] deeply frozen plugin options
|
||||
def self.options
|
||||
@options
|
||||
end
|
||||
|
||||
# @yieldparam config [Vamp::Config]
|
||||
# @yieldreturn [Object]
|
||||
# @return [Object] the block result
|
||||
def self.configure
|
||||
raise ArgumentError, "configure is only available in config files" unless @mode == "config"
|
||||
raise ArgumentError, "configure requires a block" unless block_given?
|
||||
|
||||
yield @config
|
||||
end
|
||||
|
||||
class << self
|
||||
private
|
||||
|
||||
# Clears declarations and callbacks before loading a script.
|
||||
# @return [nil]
|
||||
def reset_registrations
|
||||
@config = Config.new
|
||||
@name = nil
|
||||
@requires = []
|
||||
@commands = {}
|
||||
@async = false
|
||||
@document_callback = nil
|
||||
@shutdown_callback = nil
|
||||
end
|
||||
end
|
||||
|
||||
# Plugin declarations and hooks operate on the extending host's singleton state.
|
||||
module PluginRegistration
|
||||
# @param name [String]
|
||||
# @param requires [Array<String>] frontend capabilities
|
||||
# @param async [Boolean]
|
||||
# @return [nil]
|
||||
def plugin(name, requires: [], async: false)
|
||||
raise ArgumentError, "plugin is only available in plugin files" unless @mode == "plugin"
|
||||
raise ArgumentError, "plugin already declared" unless @name.nil?
|
||||
|
||||
name = registration_name!(name, "plugin")
|
||||
@requires = capabilities!(requires)
|
||||
raise ArgumentError, "capability must not be empty" if @requires.any?(&:empty?)
|
||||
raise TypeError, "async must be true or false" unless [true, false].include?(async)
|
||||
|
||||
@async = async
|
||||
@name = name
|
||||
nil
|
||||
end
|
||||
|
||||
# @param name [String] external command name
|
||||
# @param callback [Proc]
|
||||
# @yieldparam editor [Vamp::Editor] immutable context and queued effects
|
||||
# @yieldparam args [String] remaining command text
|
||||
# @yieldreturn [Object] ignored
|
||||
# @return [nil]
|
||||
def command(name, &callback)
|
||||
raise ArgumentError, "declare a plugin before its commands" unless @mode == "plugin" && @name
|
||||
|
||||
name = registration_name!(name, "command")
|
||||
raise ArgumentError, "command requires a block" unless callback
|
||||
raise ArgumentError, "duplicate command #{name}" if @commands.key?(name)
|
||||
|
||||
@commands[name] = callback
|
||||
nil
|
||||
end
|
||||
|
||||
# @param callback [Proc]
|
||||
# @yieldparam event [Hash{String => String, Hash{String => String, Integer, nil}}] frozen event/document
|
||||
# @yieldreturn [Object] ignored
|
||||
# @return [nil]
|
||||
def on_document(&callback)
|
||||
require_async!("document callbacks require")
|
||||
raise ArgumentError, "on_document requires a block" unless callback
|
||||
raise ArgumentError, "document callback already registered" if @document_callback
|
||||
|
||||
@document_callback = callback
|
||||
nil
|
||||
end
|
||||
|
||||
# @param callback [Proc]
|
||||
# @yieldreturn [Object] ignored
|
||||
# @return [nil]
|
||||
def on_shutdown(&callback)
|
||||
require_async!("shutdown callbacks require")
|
||||
raise ArgumentError, "on_shutdown requires a block" unless callback
|
||||
raise ArgumentError, "shutdown callback already registered" if @shutdown_callback
|
||||
|
||||
@shutdown_callback = callback
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @param name [Object]
|
||||
# @param kind [String] plugin or command
|
||||
# @return [String] validated UTF-8 name
|
||||
def registration_name!(name, kind)
|
||||
name = string!(name, "#{kind} name")
|
||||
raise ArgumentError, "#{kind} name must not be empty" if name.empty?
|
||||
|
||||
name
|
||||
end
|
||||
|
||||
# @param requires [Object]
|
||||
# @return [Array<String>] validated UTF-8 capability names
|
||||
def capabilities!(requires)
|
||||
raise TypeError, "requires must be an Array of Strings" unless requires.is_a?(Array)
|
||||
|
||||
requires.map { |value| string!(value, "capability") }
|
||||
end
|
||||
|
||||
# @param operation [String] prefix of the unchanged asynchronous-only error
|
||||
# @return [void]
|
||||
def require_async!(operation)
|
||||
return if @mode == "plugin" && @name && @async
|
||||
|
||||
raise ArgumentError, "#{operation} an asynchronous plugin"
|
||||
end
|
||||
end
|
||||
|
||||
extend PluginRegistration
|
||||
|
||||
# @return [Object, nil] callback result, or nil when no hook is registered
|
||||
def self.shutdown
|
||||
callback = @shutdown_callback
|
||||
@shutdown_callback = nil
|
||||
callback&.call
|
||||
end
|
||||
|
||||
# @param effects [Array<Hash{String, Symbol => Object}>] JSON-compatible async update effects
|
||||
# @param request_id [Integer, nil] positive unsigned 64-bit invocation ID, or background update
|
||||
# @return [nil]
|
||||
def self.publish(effects, request_id: nil)
|
||||
require_async!("publish requires")
|
||||
FrameValidation.effects!(effects)
|
||||
FrameValidation.request_id!(request_id)
|
||||
@sender.call({ "version" => 1, "type" => "update", "request_id" => request_id,
|
||||
"effects" => json_value!(effects) })
|
||||
nil
|
||||
end
|
||||
|
||||
# @param request [Hash{String => Object}] incoming document event frame
|
||||
# @return [nil]
|
||||
def self.document(request)
|
||||
raise ArgumentError, "invalid document event frame" unless @async
|
||||
|
||||
FrameValidation.document!(request)
|
||||
document = FrameValidation.snapshot!(request["document"])
|
||||
@document_callback&.call(deep_freeze({ "event" => request["event"], "document" => document }))
|
||||
nil
|
||||
end
|
||||
|
||||
# @return [Hash{String => Object}] configuration or plugin manifest frame
|
||||
def self.startup
|
||||
return @config.response if @mode == "config"
|
||||
|
||||
raise ArgumentError, "plugin file must declare Vamp.plugin" unless @name
|
||||
|
||||
{ "version" => 1, "type" => "manifest", "name" => @name,
|
||||
"requires" => @requires, "commands" => @commands.keys, "async" => @async }
|
||||
end
|
||||
|
||||
# @param request [Hash{String => Object}] invocation frame
|
||||
# @return [Hash{String => Object}] result frame with queued effects
|
||||
def self.invoke(request)
|
||||
FrameValidation.invocation!(request)
|
||||
command = string!(request["command"], "command")
|
||||
args = string!(request["args"], "args")
|
||||
callback = @commands[command]
|
||||
raise ArgumentError, "unknown command #{command}" unless callback
|
||||
|
||||
editor = Editor.new(request["context"], request["id"])
|
||||
callback.call(editor, args)
|
||||
{ "version" => 1, "type" => "result", "id" => request["id"], "effects" => editor.effects }
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user