# frozen_string_literal: true module Mongo module SearchIndex # A class representing a view of search indexes. class View include Enumerable include Retryable include Collection::Helpers # @return [ Mongo::Collection ] the collection this view belongs to attr_reader :collection # @return [ nil | String ] the index id to query attr_reader :requested_index_id # @return [ nil | String ] the index name to query attr_reader :requested_index_name # @return [ Hash ] the options hash to use for the aggregate command # when querying the available indexes. attr_reader :aggregate_options # Create the new search index view. # # @param [ Collection ] collection The collection. # @param [ Hash ] options The options that configure the behavior of the view. # # @option options [ String ] :id The specific index id to query (optional) # @option options [ String ] :name The name of the specific index to query (optional) # @option options [ Hash ] :aggregate The options hash to send to the # aggregate command when querying the available indexes. def initialize(collection, options = {}) @collection = collection @requested_index_id = options[:id] @requested_index_name = options[:name] @aggregate_options = options[:aggregate] || {} return if @aggregate_options.is_a?(Hash) raise ArgumentError, "The :aggregate option must be a Hash (got a #{@aggregate_options.class})" end # Create a single search index with the given definition. If the name is # provided, the new index will be given that name. # # @param [ Hash ] definition The definition of the search index. # @param [ nil | String ] name The name to give the new search index. # # @return [ String ] the name of the new search index. def create_one(definition, name: nil) create_many([ { name: name, definition: definition } ]).first end # Create multiple search indexes with a single command. # # @param [ Array ] indexes The description of the indexes to # create. Each element of the list must be a hash with a definition # key, and an optional name key. # # @return [ Array ] the names of the new search indexes. def create_many(indexes) spec = spec_with(indexes: indexes.map { |v| validate_search_index!(v) }) result = Operation::CreateSearchIndexes.new(spec).execute(next_primary, context: execution_context) result.first['indexesCreated'].map { |idx| idx['name'] } end # Drop the search index with the given id, or name. One or the other must # be specified, but not both. # # @param [ String ] id the id of the index to drop # @param [ String ] name the name of the index to drop # # @return [ Mongo::Operation::Result | false ] the result of the # operation, or false if the given index does not exist. def drop_one(id: nil, name: nil) validate_id_or_name!(id, name) spec = spec_with(index_id: id, index_name: name) op = Operation::DropSearchIndex.new(spec) # per the spec: # Drivers MUST suppress NamespaceNotFound errors for the # ``dropSearchIndex`` helper. Drop operations should be idempotent. do_drop(op, nil, execution_context) end # Iterate over the search indexes. # # @param [ Proc ] block if given, each search index will be yieleded to # the block. # # @return [ self | Enumerator ] if a block is given, self is returned. # Otherwise, an enumerator will be returned. def each(&block) @result ||= begin spec = {}.tap do |s| s[:id] = requested_index_id if requested_index_id s[:name] = requested_index_name if requested_index_name end collection.aggregate( [ { '$listSearchIndexes' => spec } ], aggregate_options ) end return @result.to_enum unless block @result.each(&block) self end # Update the search index with the given id or name. One or the other # must be provided, but not both. # # @param [ Hash ] definition the definition to replace the given search # index with. # @param [ nil | String ] id the id of the search index to update # @param [ nil | String ] name the name of the search index to update # # @return [ Mongo::Operation::Result ] the result of the operation def update_one(definition, id: nil, name: nil) validate_id_or_name!(id, name) spec = spec_with(index_id: id, index_name: name, index: definition) Operation::UpdateSearchIndex.new(spec).execute(next_primary, context: execution_context) end # The following methods are to make the view act more like an array, # without having to explicitly make it an array... # Queries whether the search index enumerable is empty. # # @return [ true | false ] whether the enumerable is empty or not. def empty? count.zero? end private # A helper method for building the specification document with certain # values pre-populated. # # @param [ Hash ] extras the values to put into the specification # # @return [ Hash ] the specification document def spec_with(extras) { coll_name: collection.name, db_name: collection.database.name, }.merge(extras) end # A helper method for retrieving the primary server from the cluster. # # @return [ Mongo::Server ] the server to use def next_primary(ping = nil, session = nil) collection.cluster.next_primary(ping, session) end # A helper method for constructing a new operation context for executing # an operation. # # @return [ Mongo::Operation::Context ] the operation context def execution_context Operation::Context.new(client: collection.client) end # Validates the given id and name, ensuring that exactly one of them # is non-nil. # # @param [ nil | String ] id the id to validate # @param [ nil | String ] name the name to validate # # @raise [ ArgumentError ] if neither or both arguments are nil def validate_id_or_name!(id, name) return unless (id.nil? && name.nil?) || (!id.nil? && !name.nil?) raise ArgumentError, 'exactly one of id or name must be specified' end # Validates the given search index document, ensuring that it has no # extra keys, and that the name and definition are valid. # # @param [ Hash ] doc the document to validate # # @raise [ ArgumentError ] if the document is invalid. def validate_search_index!(doc) validate_search_index_keys!(doc.keys) validate_search_index_name!(doc[:name] || doc['name']) validate_search_index_definition!(doc[:definition] || doc['definition']) doc end # Validates the keys of a search index document, ensuring that # they are all valid. # # @param [ Array ] keys the keys of a search index document # # @raise [ ArgumentError ] if the list contains any invalid keys def validate_search_index_keys!(keys) extras = keys - [ 'name', 'definition', :name, :definition ] raise ArgumentError, "invalid keys in search index creation: #{extras.inspect}" if extras.any? end # Validates the name of a search index, ensuring that it is either a # String or nil. # # @param [ nil | String ] name the name of a search index # # @raise [ ArgumentError ] if the name is not valid def validate_search_index_name!(name) return if name.nil? || name.is_a?(String) raise ArgumentError, "search index name must be nil or a string (got #{name.inspect})" end # Validates the definition of a search index. # # @param [ Hash ] definition the definition of a search index # # @raise [ ArgumentError ] if the definition is not valid def validate_search_index_definition!(definition) return if definition.is_a?(Hash) raise ArgumentError, "search index definition must be a Hash (got #{definition.inspect})" end end end end