Files
vamp/src/ruby_values.rb
T

213 lines
6.8 KiB
Ruby

# frozen_string_literal: true
# Shared value validation and frozen editor-owned configuration objects.
module Vamp
MAX_FRAME = 4 * 1024 * 1024
# Validates a boundary value and returns a UTF-8 copy.
# @param value [Object]
# @param label [String]
# @return [String]
def self.string!(value, label)
raise TypeError, "#{label} must be a String" unless value.is_a?(String)
copy = value.dup.force_encoding(Encoding::UTF_8)
raise ArgumentError, "#{label} must be valid UTF-8" unless copy.valid_encoding?
copy
end
# Recursively validates JSON values and normalizes hash keys to strings.
# @param value [Object]
# @return [Hash{String => Object}, Array<Object>, String, Integer, Float, Boolean, nil]
def self.json_value!(value)
case value
when Hash
json_hash(value)
when Array
value.map { |item| json_value!(item) }
else
json_scalar(value)
end
end
# Freezes the supplied value and every nested hash/array member in place.
# @param value [Object]
# @return [Object] the same value
def self.deep_freeze(value)
case value
when Hash
value.each do |key, item|
deep_freeze(key)
deep_freeze(item)
end
when Array
value.each { |item| deep_freeze(item) }
end
value.freeze
end
# @!override Vamp::Action#name
# @return [String] the catalog's dotted action name
Action = Struct.new(:name)
# @!override Vamp::Mode#name
# @return [String] the lowercase binding mode
Mode = Struct.new(:name)
# Action methods and nested modules come exclusively from the Rust catalog.
# @type [Module<Vamp::Core>]
Core = Module.new
# @param entries [Array<Hash{String => String, nil}>] action name and optional canonical Ex name
# @param command_aliases [Hash{String => String}] shorthand to canonical commands
# @return [Array<Module, Vamp::Action>] frozen action namespaces and leaves
def self.install_actions(entries, command_aliases)
raise TypeError, "core action catalog must be an Array" unless entries.is_a?(Array)
raise TypeError, "command aliases must be a Hash" unless command_aliases.is_a?(Hash)
namespaces = [Core]
commands = {}
entries.sort_by { |entry| entry.fetch("name").count(".") }.each do |entry|
raise TypeError, "core action descriptor must be a Hash" unless entry.is_a?(Hash)
install_action(entry.fetch("name"), namespaces)
ex = entry.fetch("ex")
canonical = string!(ex, "core Ex command").freeze unless ex.nil?
commands[canonical] = canonical unless ex.nil?
end
canonical_commands = commands.keys
command_aliases.each do |name, canonical|
name = string!(name, "command alias").freeze
canonical = string!(canonical, "canonical command").freeze
raise ArgumentError, "unknown canonical command: #{canonical}" unless canonical_commands.include?(canonical)
commands[name] = canonical
end
@builtin_commands = commands.freeze
namespaces.each(&:freeze)
end
# @param name [String] binding command name
# @return [Boolean] whether the catalog owns this canonical command or shorthand
def self.builtin_command?(name)
@builtin_commands.key?(name)
end
# Frozen binding mode values, distinct from a buffer's editing state.
module Modes
# @!method self.normal
# @return [Vamp::Mode]
# @!method self.insert
# @return [Vamp::Mode]
# @!method self.command
# @return [Vamp::Mode]
# @!method self.search
# @return [Vamp::Mode]
# @!method self.visual
# @return [Vamp::Mode]
# @!method self.selection
# @return [Vamp::Mode]
# @!method self.tree
# @return [Vamp::Mode]
%w[normal insert command search visual selection tree].each do |name|
mode = Mode.new(name.freeze).freeze
define_singleton_method(name) { mode }
end
end
Action.private_class_method :new
Mode.private_class_method :new
# @param value [Hash{String, Symbol => Object}]
# @return [Hash{String => Object}]
def self.json_hash(value)
value.to_h do |key, item|
raise TypeError, "option keys must be Strings or Symbols" unless key.is_a?(String) || key.is_a?(Symbol)
[string!(key.to_s, "option key"), json_value!(item)]
end
end
# @param value [Object]
# @return [String, Integer, Float, Boolean, nil]
def self.json_scalar(value)
case value
when String
string!(value, "option value")
when Integer, TrueClass, FalseClass, NilClass, Float
json_literal(value)
else
raise TypeError, "options must contain only JSON values"
end
end
# @param value [Integer, Float, Boolean, nil]
# @return [Integer, Float, Boolean, nil]
def self.json_literal(value)
raise ArgumentError, "option numbers must be finite" if value.is_a?(Float) && !value.finite?
value
end
# @param name [String]
# @param namespaces [Array<Module, Vamp::Action>]
# @return [Symbol] the installed action method name
def self.install_action(name, namespaces)
name = action_name(name)
parts = name.split(".").drop(1)
namespace = parts[0...-1].reduce(Core) { |parent, part| action_namespace(parent, part, namespaces, name) }
leaf = parts.last
available_action!(namespace, leaf, name)
action = Action.send(:new, name.freeze)
namespaces << action
namespace.define_singleton_method(leaf) { action }
end
# @param name [Object]
# @return [String]
def self.action_name(name)
name = string!(name, "core action name")
raise ArgumentError, "invalid core action name: #{name}" unless name.match?(/\Acore(?:\.[a-z_][a-z0-9_]*)+\z/)
name
end
# @param parent [Module, Vamp::Action]
# @param part [String]
# @param namespaces [Array<Module, Vamp::Action>]
# @param name [String]
# @return [Module, Vamp::Action]
def self.action_namespace(parent, part, namespaces, name)
return new_action_namespace(parent, part, namespaces) unless parent.singleton_methods(false).include?(part.to_sym)
child = parent.public_send(part)
raise ArgumentError, "conflicting core action namespace: #{name}" unless child.is_a?(Module) || child.is_a?(Action)
child
end
# @param parent [Module, Vamp::Action]
# @param part [String]
# @param namespaces [Array<Module, Vamp::Action>]
# @return [Module]
def self.new_action_namespace(parent, part, namespaces)
child = Module.new
parent.define_singleton_method(part) { child }
namespaces << child
child
end
# @param namespace [Module, Vamp::Action]
# @param leaf [String]
# @param name [String]
# @return [nil]
def self.available_action!(namespace, leaf, name)
return unless namespace.singleton_methods(false).include?(leaf.to_sym)
raise ArgumentError, "conflicting core action name: #{name}"
end
private_class_method :json_hash, :json_scalar, :json_literal, :install_action, :action_name,
:action_namespace, :new_action_namespace, :available_action!
end