# 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, 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] Core = Module.new # @param entries [Array String, nil}>] action name and optional canonical Ex name # @param command_aliases [Hash{String => String}] shorthand to canonical commands # @return [Array] 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] # @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] # @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] # @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