diff --git a/elasticsearch-persistence/.rspec b/elasticsearch-persistence/.rspec new file mode 100644 index 000000000..77d185827 --- /dev/null +++ b/elasticsearch-persistence/.rspec @@ -0,0 +1,2 @@ +--tty +--colour diff --git a/elasticsearch-persistence/Gemfile b/elasticsearch-persistence/Gemfile index c88afe19f..de011df05 100644 --- a/elasticsearch-persistence/Gemfile +++ b/elasticsearch-persistence/Gemfile @@ -6,3 +6,8 @@ gemspec gem 'elasticsearch-model', :path => File.expand_path("../../elasticsearch-model", __FILE__), :require => false gem 'virtus' + +group :development, :testing do + gem 'rspec' + gem 'pry-nav' +end diff --git a/elasticsearch-persistence/Rakefile b/elasticsearch-persistence/Rakefile index 61038d08c..660e57211 100644 --- a/elasticsearch-persistence/Rakefile +++ b/elasticsearch-persistence/Rakefile @@ -7,24 +7,24 @@ task :test => 'test:unit' # ----- Test tasks ------------------------------------------------------------ require 'rake/testtask' +require 'rspec/core/rake_task' + namespace :test do + + RSpec::Core::RakeTask.new(:spec) Rake::TestTask.new(:unit) do |test| - test.libs << 'lib' << 'test' - test.test_files = FileList["test/unit/**/*_test.rb"] - test.verbose = false - test.warning = false end Rake::TestTask.new(:integration) do |test| - test.libs << 'lib' << 'test' - test.test_files = FileList["test/integration/**/*_test.rb"] test.verbose = false test.warning = false + test.deps = [ :spec ] end Rake::TestTask.new(:all) do |test| - test.libs << 'lib' << 'test' - test.test_files = FileList["test/unit/**/*_test.rb", "test/integration/**/*_test.rb"] + test.verbose = false + test.warning = false + test.deps = [ :spec ] end end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence.rb b/elasticsearch-persistence/lib/elasticsearch/persistence.rb index 986ef1bc4..ce0d25b60 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence.rb @@ -1,116 +1,8 @@ require 'hashie/mash' require 'elasticsearch' - -require 'elasticsearch/model/hash_wrapper' -require 'elasticsearch/model/indexing' -require 'elasticsearch/model/searching' - -require 'active_support/inflector' +require 'elasticsearch/model' require 'elasticsearch/persistence/version' - -require 'elasticsearch/persistence/client' -require 'elasticsearch/persistence/repository/response/results' -require 'elasticsearch/persistence/repository/naming' -require 'elasticsearch/persistence/repository/serialize' -require 'elasticsearch/persistence/repository/store' -require 'elasticsearch/persistence/repository/find' -require 'elasticsearch/persistence/repository/search' -require 'elasticsearch/persistence/repository/class' require 'elasticsearch/persistence/repository' - -module Elasticsearch - - # Persistence for Ruby domain objects and models in Elasticsearch - # =============================================================== - # - # `Elasticsearch::Persistence` contains modules for storing and retrieving Ruby domain objects and models - # in Elasticsearch. - # - # == Repository - # - # The repository patterns allows to store and retrieve Ruby objects in Elasticsearch. - # - # require 'elasticsearch/persistence' - # - # class Note - # def to_hash; {foo: 'bar'}; end - # end - # - # repository = Elasticsearch::Persistence::Repository.new - # - # repository.save Note.new - # # => {"_index"=>"repository", "_type"=>"note", "_id"=>"mY108X9mSHajxIy2rzH2CA", ...} - # - # Customize your repository by including the main module in a Ruby class - # class MyRepository - # include Elasticsearch::Persistence::Repository - # - # index 'my_notes' - # klass Note - # - # client Elasticsearch::Client.new log: true - # end - # - # repository = MyRepository.new - # - # repository.save Note.new - # # 2014-04-04 22:15:25 +0200: POST http://localhost:9200/my_notes/note [status:201, request:0.009s, query:n/a] - # # 2014-04-04 22:15:25 +0200: > {"foo":"bar"} - # # 2014-04-04 22:15:25 +0200: < {"_index":"my_notes","_type":"note","_id":"-d28yXLFSlusnTxb13WIZQ", ...} - # - # == Model - # - # The active record pattern allows to use the interface familiar from ActiveRecord models: - # - # require 'elasticsearch/persistence' - # - # class Article - # attribute :title, String, mapping: { analyzer: 'snowball' } - # end - # - # article = Article.new id: 1, title: 'Test' - # article.save - # - # Article.find(1) - # - # article.update_attributes title: 'Update' - # - # article.destroy - # - module Persistence - - # :nodoc: - module ClassMethods - - # Get or set the default client for all repositories and models - # - # @example Set and configure the default client - # - # Elasticsearch::Persistence.client Elasticsearch::Client.new host: 'http://localhost:9200', tracer: true - # - # @example Perform an API request through the client - # - # Elasticsearch::Persistence.client.cluster.health - # # => { "cluster_name" => "elasticsearch" ... } - # - def client client=nil - @client = client || @client || Elasticsearch::Client.new - end - - # Set the default client for all repositories and models - # - # @example Set and configure the default client - # - # Elasticsearch::Persistence.client = Elasticsearch::Client.new host: 'http://localhost:9200', tracer: true - # => # - # - def client=(client) - @client = client - end - end - - extend ClassMethods - end -end +require 'elasticsearch/persistence/repository/response/results' diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/client.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/client.rb deleted file mode 100644 index 3c9d618d4..000000000 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/client.rb +++ /dev/null @@ -1,51 +0,0 @@ -module Elasticsearch - module Persistence - module Repository - - # Wraps the Elasticsearch Ruby - # [client](https://github.com/elasticsearch/elasticsearch-ruby/tree/master/elasticsearch#usage) - # - module Client - - # Get or set the default client for this repository - # - # @example Set and configure the client for the repository class - # - # class MyRepository - # include Elasticsearch::Persistence::Repository - # client Elasticsearch::Client.new host: 'http://localhost:9200', log: true - # end - # - # @example Set and configure the client for this repository instance - # - # repository.client Elasticsearch::Client.new host: 'http://localhost:9200', tracer: true - # - # @example Perform an API request through the client - # - # MyRepository.client.cluster.health - # repository.client.cluster.health - # # => { "cluster_name" => "elasticsearch" ... } - # - def client client=nil - @client = client || @client || Elasticsearch::Persistence.client - end - - # Set the default client for this repository - # - # @example Set and configure the client for the repository class - # - # MyRepository.client = Elasticsearch::Client.new host: 'http://localhost:9200', log: true - # - # @example Set and configure the client for this repository instance - # - # repository.client = Elasticsearch::Client.new host: 'http://localhost:9200', tracer: true - # - def client=(client) - @client = client - @client - end - end - - end - end -end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb index f6a202029..33cbdc2c4 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb @@ -1,77 +1,224 @@ +require 'elasticsearch/persistence/repository/dsl' +require 'elasticsearch/persistence/repository/find' +require 'elasticsearch/persistence/repository/store' +require 'elasticsearch/persistence/repository/serialize' +require 'elasticsearch/persistence/repository/search' + module Elasticsearch module Persistence - # Delegate methods to the repository (acting as a gateway) + # The base Repository mixin. This module should be included in classes that + # represent an Elasticsearch repository. # - module GatewayDelegation - def method_missing(method_name, *arguments, &block) - gateway.respond_to?(method_name) ? gateway.__send__(method_name, *arguments, &block) : super - end + # @since 6.0.0 + module Repository + include Store + include Serialize + include Find + include Search + include Elasticsearch::Model::Indexing::ClassMethods - def respond_to?(method_name, include_private=false) - gateway.respond_to?(method_name) || super + def self.included(base) + base.send(:extend, ClassMethods) end - def respond_to_missing?(method_name, *) - gateway.respond_to?(method_name) || super - end - end + module ClassMethods - # When included, creates an instance of the {Repository::Class Repository} class as a "gateway" - # - # @example Include the repository in a custom class - # - # require 'elasticsearch/persistence' - # - # class MyRepository - # include Elasticsearch::Persistence::Repository - # end - # - module Repository - def self.included(base) - gateway = Elasticsearch::Persistence::Repository::Class.new host: base - - # Define the instance level gateway + # Initialize a repository instance. Optionally provide a block to define index mappings or + # settings on the repository instance. + # + # @example Create a new repository. + # MyRepository.create(index_name: 'notes', klass: Note) + # + # @example Create a new repository and evaluate a block on it. + # MyRepository.create(index_name: 'notes', klass: Note) do + # mapping dynamic: 'strict' do + # indexes :title + # end + # end # - base.class_eval do - define_method :gateway do - @gateway ||= gateway + # @param [ Hash ] options The options to use. + # @param [ Proc ] block A block to evaluate on the new repository instance. + # + # @option options [ Symbol, String ] :index_name The name of the index. + # @option options [ Symbol, String ] :document_type The type of documents persisted in this repository. + # @option options [ Symbol, String ] :client The client used to handle requests to and from Elasticsearch. + # @option options [ Symbol, String ] :klass The class used to instantiate an object when documents are + # deserialized. The default is nil, in which case the raw document will be returned as a Hash. + # @option options [ Elasticsearch::Model::Indexing::Mappings, Hash ] :mapping The mapping for this index. + # @option options [ Elasticsearch::Model::Indexing::Settings, Hash ] :settings The settings for this index. + # + # @since 6.0.0 + def create(options = {}, &block) + new(options).tap do |obj| + obj.instance_eval(&block) if block_given? end - - include GatewayDelegation end + end - # Define the class level gateway - # - (class << base; self; end).class_eval do - define_method :gateway do |&block| - @gateway ||= gateway - @gateway.instance_eval(&block) if block - @gateway - end + # The default index name. + # + # @return [ String ] The default index name. + # + # @since 6.0.0 + DEFAULT_INDEX_NAME = 'repository'.freeze - include GatewayDelegation - end + # The default document type. + # + # @return [ String ] The default document type. + # + # @note the document type will no longer be configurable in future versions + # of Elasticsearch. + # + # @since 6.0.0 + DEFAULT_DOC_TYPE = '_doc'.freeze - # Catch repository methods (such as `serialize` and others) defined in the receiving class, - # and overload the default definition in the gateway - # - def base.method_added(name) - if :gateway != name && respond_to?(:gateway) && (gateway.public_methods - Object.public_methods).include?(name) - gateway.define_singleton_method(name, self.new.method(name).to_proc) + # The repository options. + # + # @return [ Hash ] + # + # @since 6.0.0 + attr_reader :options + + # Initialize a repository instance. + # + # @example Initialize the repository. + # MyRepository.new(index_name: 'notes', klass: Note) + # + # @param [ Hash ] options The options to use. + # + # @option options [ Symbol, String ] :index_name The name of the index. + # @option options [ Symbol, String ] :document_type The type of documents persisted in this repository. + # @option options [ Symbol, String ] :client The client used to handle requests to and from Elasticsearch. + # @option options [ Symbol, String ] :klass The class used to instantiate an object when documents are + # deserialized. The default is nil, in which case the raw document will be returned as a Hash. + # @option options [ Elasticsearch::Model::Indexing::Mappings, Hash ] :mapping The mapping for this index. + # @option options [ Elasticsearch::Model::Indexing::Settings, Hash ] :settings The settings for this index. + # + # @since 6.0.0 + def initialize(options = {}) + @options = options + end + + # Get the client used by the repository. + # + # @example + # repository.client + # + # @return [ Elasticsearch::Transport::Client ] The repository's client. + # + # @since 6.0.0 + def client + @client ||= @options[:client] || + __get_class_value(:client) || + Elasticsearch::Transport::Client.new + end + + # Get the document type used by the repository object. + # + # @example + # repository.document_type + # + # @return [ String, Symbol ] The repository's document type. + # + # @since 6.0.0 + def document_type + @document_type ||= @options[:document_type] || + __get_class_value(:document_type) || + DEFAULT_DOC_TYPE + end + + # Get the index name used by the repository. + # + # @example + # repository.index_name + # + # @return [ String, Symbol ] The repository's index name. + # + # @since 6.0.0 + def index_name + @index_name ||= @options[:index_name] || + __get_class_value(:index_name) || + DEFAULT_INDEX_NAME + end + + # Get the class used by the repository when deserializing. + # + # @example + # repository.klass + # + # @return [ Class ] The repository's klass for deserializing. + # + # @since 6.0.0 + def klass + @klass ||= @options[:klass] || __get_class_value(:klass) + end + + # Get the index mapping. Optionally pass a block to define the mappings. + # + # @example + # repository.mapping + # + # @example Set the mappings with a block. + # repository.mapping dynamic: 'strict' do + # indexes :foo + # end + # end + # + # @note If mappings were set when the repository was created, a block passed to this + # method will not be evaluated. + # + # @return [ Elasticsearch::Model::Indexing::Mappings ] The index mappings. + # + # @since 6.0.0 + def mapping(*args) + @memoized_mapping ||= @options[:mapping] || (begin + if _mapping = __get_class_value(:mapping) + _mapping.instance_variable_set(:@type, document_type) + _mapping end - end + end) || (super && @mapping) + end + alias :mappings :mapping + + # Get the index settings. + # + # @example + # repository.settings + # + # @example Set the settings with a block. + # repository.settings number_of_shards: 1, number_of_replicas: 0 do + # mapping dynamic: 'strict' do + # indexes :foo do + # indexes :bar + # end + # end + # end + # + # @return [ Elasticsearch::Model::Indexing::Settings ] The index settings. + # + # @since 6.0.0 + def settings(*args) + @memoized_settings ||= @options[:settings] || __get_class_value(:settings) || (super && @settings) end - # Shortcut method to allow concise repository initialization + # Determine whether the index with this repository's index name exists. # - # @example Create a new default repository + # @example + # repository.index_exists? # - # repository = Elasticsearch::Persistence::Repository.new + # @return [ true, false ] Whether the index exists. # - def new(options={}, &block) - Elasticsearch::Persistence::Repository::Class.new( {index: 'repository'}.merge(options), &block ) - end; module_function :new + # @since 6.0.0 + def index_exists?(*args) + super(index_name: index_name) + end + + private + + def __get_class_value(_method_) + self.class.send(_method_) if self.class.respond_to?(_method_) + end end end end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb deleted file mode 100644 index 0dcd2d576..000000000 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb +++ /dev/null @@ -1,71 +0,0 @@ -module Elasticsearch - module Persistence - module Repository - - # The default repository class, to be used either directly, or as a gateway in a custom repository class - # - # @example Standalone use - # - # repository = Elasticsearch::Persistence::Repository::Class.new - # # => # - # repository.save(my_object) - # # => {"_index"=> ... } - # - # @example Shortcut use - # - # repository = Elasticsearch::Persistence::Repository.new - # # => # - # - # @example Configuration via a block - # - # repository = Elasticsearch::Persistence::Repository.new do - # index 'my_notes' - # end - # - # # => # - # # > repository.save(my_object) - # # => {"_index"=> ... } - # - # @example Accessing the gateway in a custom class - # - # class MyRepository - # include Elasticsearch::Persistence::Repository - # end - # - # repository = MyRepository.new - # - # repository.gateway.client.info - # # => {"status"=>200, "name"=>"Venom", ... } - # - class Class - include Elasticsearch::Persistence::Repository::Client - include Elasticsearch::Persistence::Repository::Naming - include Elasticsearch::Persistence::Repository::Serialize - include Elasticsearch::Persistence::Repository::Store - include Elasticsearch::Persistence::Repository::Find - include Elasticsearch::Persistence::Repository::Search - - include Elasticsearch::Model::Indexing::ClassMethods - - attr_reader :options - - def initialize(options={}, &block) - @options = options - index_name options.delete(:index) - block.arity < 1 ? instance_eval(&block) : block.call(self) if block_given? - end - - # Return the "host" class, if this repository is a gateway hosted in another class - # - # @return [nil, Class] - # - # @api private - # - def host - options[:host] - end - end - - end - end -end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/dsl.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/dsl.rb new file mode 100644 index 000000000..0c880d7ec --- /dev/null +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/dsl.rb @@ -0,0 +1,94 @@ +module Elasticsearch + module Persistence + module Repository + + # Include this module to get class-level methods for repository configuration. + # + # @since 6.0.0 + module DSL + + def self.included(base) + base.send(:extend, Elasticsearch::Model::Indexing::ClassMethods) + base.send(:extend, ClassMethods) + end + + # These methods are necessary to define at the class-level so that the methods available + # via Elasticsearch::Model::Indexing::ClassMethods have the references they depend on. + # + # @since 6.0.0 + module ClassMethods + + # Get or set the class-level document type setting. + # + # @example + # MyRepository.document_type + # + # @return [ String, Symbol ] _type The repository's document type. + # + # @since 6.0.0 + def document_type(_type = nil) + @document_type ||= (_type || DEFAULT_DOC_TYPE) + end + + # Get or set the class-level index name setting. + # + # @example + # MyRepository.index_name + # + # @return [ String, Symbol ] _name The repository's index name. + # + # @since 6.0.0 + def index_name(_name = nil) + @index_name ||= (_name || DEFAULT_INDEX_NAME) + end + + # Get or set the class-level setting for the class used by the repository when deserializing. + # + # @example + # MyRepository.klass + # + # @return [ Class ] _class The repository's klass for deserializing. + # + # @since 6.0.0 + def klass(_class = nil) + instance_variables.include?(:@klass) ? @klass : @klass = _class + end + + # Get or set the class-level setting for the client used by the repository. + # + # @example + # MyRepository.client + # + # @return [ Class ] _client The repository's client. + # + # @since 6.0.0 + def client(_client = nil) + @client ||= (_client || Elasticsearch::Transport::Client.new) + end + + def create_index!(*args) + __raise_not_implemented_error(__method__) + end + + def delete_index!(*args) + __raise_not_implemented_error(__method__) + end + + def refresh_index!(*args) + __raise_not_implemented_error(__method__) + end + + def index_exists?(*args) + __raise_not_implemented_error(__method__) + end + + private + + def __raise_not_implemented_error(_method_) + raise NotImplementedError, "The '#{_method_}' method is not implemented on the Repository class." + end + end + end + end + end +end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb index 1ea0233fd..cbd264ae2 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb @@ -7,16 +7,6 @@ class DocumentNotFound < StandardError; end # module Find - # The key for accessing the document found and returned from an - # Elasticsearch _mget query. - # - DOCS = 'docs'.freeze - - # The key for the boolean value indicating whether a particular id - # has been successfully found in an Elasticsearch _mget query. - # - FOUND = 'found'.freeze - # Retrieve a single object or multiple objects from Elasticsearch by ID or IDs # # @example Retrieve a single object by ID @@ -50,6 +40,9 @@ def find(*args) # repository.exists?(1) # => true # + # @param [ String, Integer ] id The id to search. + # @param [ Hash ] options The options. + # # @return [true, false] # def exists?(id, options={}) @@ -58,6 +51,18 @@ def exists?(id, options={}) client.exists(request.merge(options)) end + private + + # The key for accessing the document found and returned from an + # Elasticsearch _mget query. + # + DOCS = 'docs'.freeze + + # The key for the boolean value indicating whether a particular id + # has been successfully found in an Elasticsearch _mget query. + # + FOUND = 'found'.freeze + # @api private # def __find_one(id, options={}) @@ -75,7 +80,9 @@ def __find_many(ids, options={}) request = { index: index_name, body: { ids: ids } } request[:type] = document_type if document_type documents = client.mget(request.merge(options)) - documents[DOCS].map { |document| document[FOUND] ? deserialize(document) : nil } + documents[DOCS].map do |document| + deserialize(document) if document[FOUND] + end end end end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb deleted file mode 100644 index d559d8d51..000000000 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb +++ /dev/null @@ -1,99 +0,0 @@ -module Elasticsearch - module Persistence - module Repository - - # Wraps all naming-related features of the repository (index name, the domain object class, etc) - # - module Naming - - # The possible keys for a document id. - # - IDS = [:id, 'id', :_id, '_id'].freeze - - DEFAULT_DOC_TYPE = '_doc'.freeze - - # Get or set the class used to initialize domain objects when deserializing them - # - def klass(name=nil) - if name - @klass = name - else - @klass - end - end - - # Set the class used to initialize domain objects when deserializing them - # - def klass=klass - @klass = klass - end - - # Get or set the index name used when storing and retrieving documents - # - def index_name name=nil - @index_name = name || @index_name || begin - if respond_to?(:host) && host && host.is_a?(Module) - self.host.to_s.underscore.gsub(/\//, '-') - else - self.class.to_s.underscore.gsub(/\//, '-') - end - end - end; alias :index :index_name - - # Set the index name used when storing and retrieving documents - # - def index_name=(name) - @index_name = name - end; alias :index= :index_name= - - # Get or set the document type used when storing and retrieving documents - # - def document_type name=nil - @document_type = name || @document_type || DEFAULT_DOC_TYPE - end; alias :type :document_type - - # Set the document type used when storing and retrieving documents - # - def document_type=(name) - @document_type = name - end; alias :type= :document_type= - - # Get a document ID from the document (assuming Hash or Hash-like object) - # - # @example - # repository.__get_id_from_document title: 'Test', id: 'abc123' - # => "abc123" - # - # @api private - # - def __get_id_from_document(document) - document[IDS.find { |id| document[id] }] - end - - # Extract a document ID from the document (assuming Hash or Hash-like object) - # - # @note Calling this method will *remove* the `id` or `_id` key from the passed object. - # - # @example - # options = { title: 'Test', id: 'abc123' } - # repository.__extract_id_from_document options - # # => "abc123" - # options - # # => { title: 'Test' } - # - # @api private - # - def __extract_id_from_document(document) - IDS.inject(nil) do |deleted, id| - if document[id] - document.delete(id) - else - deleted - end - end - end - end - - end - end -end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb index 07c0e4d3d..203a7e246 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb @@ -6,10 +6,6 @@ module Repository # module Search - # The key for accessing the count in a Elasticsearch query response. - # - COUNT = 'count'.freeze - # Returns a collection of domain objects by an Elasticsearch query # # Pass the query either as a string or a Hash-like object @@ -41,6 +37,9 @@ module Search # # GET http://localhost:9200/notes/note/_search # # > {"query":{"match":{"title":"fox dog"}},"size":25} # + # @param [ Hash, String ] query_or_definition The query or search definition. + # @param [ Hash ] options The search options. + # # @return [Elasticsearch::Persistence::Repository::Response::Results] # def search(query_or_definition, options={}) @@ -75,6 +74,9 @@ def search(query_or_definition, options={}) # repository.search(query: { match: { title: 'fox dog' } }) # # => 1 # + # @param [ Hash, String ] query_or_definition The query or search definition. + # @param [ Hash ] options The search options. + # # @return [Integer] # def count(query_or_definition=nil, options={}) @@ -92,8 +94,13 @@ def count(query_or_definition=nil, options={}) response[COUNT] end - end + private + + # The key for accessing the count in a Elasticsearch query response. + # + COUNT = 'count'.freeze + end end end end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/serialize.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/serialize.rb index e9a8d875e..067a7daad 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/serialize.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/serialize.rb @@ -2,19 +2,40 @@ module Elasticsearch module Persistence module Repository - # Provide serialization and deserialization between Ruby objects and Elasticsearch documents + # Provide serialization and deserialization between Ruby objects and Elasticsearch documents. # # Override these methods in your repository class to customize the logic. # module Serialize - # Error message raised when documents are attempted to be deserialized and no klass is defined for - # the Repository. + # Serialize the object for storing it in Elasticsearch. # - # @since 6.0.0 - NO_CLASS_ERROR_MESSAGE = "No class is defined for deserializing documents. " + - "Please define a 'klass' for the Repository or define a custom " + - "deserialize method.".freeze + # In the default implementation, call the `to_hash` method on the passed object. + # + # @param [ Object ] document The Ruby object to serialize. + # + # @return [ Hash ] The serialized document. + # + def serialize(document) + document.to_hash + end + + # Deserialize the document retrieved from Elasticsearch into a Ruby object. + # If no klass is set for the Repository then the raw document '_source' field will be returned. + # + # def deserialize(document) + # Note.new document[SOURCE] + # end + # + # @param [ Hash ] document The raw document. + # + # @return [ Object ] The deserialized object. + # + def deserialize(document) + klass ? klass.new(document[SOURCE]) : document[SOURCE] + end + + private # The key for document fields in an Elasticsearch query response. # @@ -26,21 +47,41 @@ module Serialize # TYPE = '_type'.freeze - # Serialize the object for storing it in Elasticsearch + IDS = [:id, 'id', :_id, '_id'].freeze + + # Get a document ID from the document (assuming Hash or Hash-like object) # - # In the default implementation, call the `to_hash` method on the passed object. + # @example + # repository.__get_id_from_document title: 'Test', id: 'abc123' + # => "abc123" # - def serialize(document) - document.to_hash + # @api private + # + def __get_id_from_document(document) + document[IDS.find { |id| document[id] }] end - # Deserialize the document retrieved from Elasticsearch into a Ruby object + # Extract a document ID from the document (assuming Hash or Hash-like object) # - # Use the `klass` property, if defined, otherwise try to get the class from the document's `_type`. + # @note Calling this method will *remove* the `id` or `_id` key from the passed object. # - def deserialize(document) - raise NameError.new(NO_CLASS_ERROR_MESSAGE) unless klass - klass.new document[SOURCE] + # @example + # options = { title: 'Test', id: 'abc123' } + # repository.__extract_id_from_document options + # # => "abc123" + # options + # # => { title: 'Test' } + # + # @api private + # + def __extract_id_from_document(document) + IDS.inject(nil) do |deleted, id| + if document[id] + document.delete(id) + else + deleted + end + end end end end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb index 2327d23a6..6571e1078 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb @@ -12,13 +12,19 @@ module Store # repository.save(myobject) # => {"_index"=>"...", "_type"=>"...", "_id"=>"...", "_version"=>1, "created"=>true} # - # @return {Hash} The response from Elasticsearch + # @param [ Object ] document The document to save into Elasticsearch. + # @param [ Hash ] options The save request options. + # + # @return [ Hash ] The response from Elasticsearch # def save(document, options={}) serialized = serialize(document) - id = __get_id_from_document(serialized) - type = document_type - client.index( { index: index_name, type: type, id: id, body: serialized }.merge(options) ) + id = __get_id_from_document(serialized) + request = { index: index_name, + id: id, + body: serialized } + request[:type] = document_type if document_type + client.index(request.merge(options)) end # Update the serialized object in Elasticsearch with partial data or script @@ -33,38 +39,27 @@ def save(document, options={}) # repository.update 1, script: 'ctx._source.views += 1' # # => {"_index"=>"...", "_type"=>"...", "_id"=>"1", "_version"=>3} # - # @return {Hash} The response from Elasticsearch + # @param [ Object ] document_or_id The document to update or the id of the document to update. + # @param [ Hash ] options The update request options. # - def update(document, options={}) - case - when document.is_a?(String) || document.is_a?(Integer) - id = document - when document.respond_to?(:to_hash) - serialized = document.to_hash - id = __extract_id_from_document(serialized) - else - raise ArgumentError, "Expected a document ID or a Hash-like object, #{document.class} given" - end - - type = options.delete(:type) || \ - (defined?(serialized) && serialized && serialized.delete(:type)) || \ - document_type - - if defined?(serialized) && serialized - body = if serialized[:script] - serialized.select { |k, v| [:script, :params, :upsert].include? k } - else - { doc: serialized } - end + # @return [ Hash ] The response from Elasticsearch + # + def update(document_or_id, options = {}) + if document_or_id.is_a?(String) || document_or_id.is_a?(Integer) + id = document_or_id + body = options + type = document_type else - body = {} - body.update( doc: options.delete(:doc)) if options[:doc] - body.update( script: options.delete(:script)) if options[:script] - body.update( params: options.delete(:params)) if options[:params] - body.update( upsert: options.delete(:upsert)) if options[:upsert] + document = serialize(document_or_id) + id = __extract_id_from_document(document) + if options[:script] + body = options + else + body = { doc: document }.merge(options) + end + type = document.delete(:type) || document_type end - - client.update( { index: index_name, type: type, id: id, body: body }.merge(options) ) + client.update(index: index_name, id: id, type: type, body: body) end # Remove the serialized object or document with specified ID from Elasticsearch @@ -74,21 +69,21 @@ def update(document, options={}) # repository.delete(1) # # => {"_index"=>"...", "_type"=>"...", "_id"=>"1", "_version"=>4} # - # @return {Hash} The response from Elasticsearch + # @param [ Object ] document_or_id The document to delete or the id of the document to delete. + # @param [ Hash ] options The delete request options. # - def delete(document, options={}) - if document.is_a?(String) || document.is_a?(Integer) - id = document - type = document_type + # @return [ Hash ] The response from Elasticsearch + # + def delete(document_or_id, options = {}) + if document_or_id.is_a?(String) || document_or_id.is_a?(Integer) + id = document_or_id else - serialized = serialize(document) - id = __get_id_from_document(serialized) - type = document_type + serialized = serialize(document_or_id) + id = __get_id_from_document(serialized) end - client.delete( { index: index_name, type: type, id: id }.merge(options) ) + client.delete({ index: index_name, type: document_type, id: id }.merge(options)) end end - end end end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/version.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/version.rb index 862c815a6..fea31b99c 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/version.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/version.rb @@ -1,5 +1,5 @@ module Elasticsearch module Persistence - VERSION = "6.0.0" + VERSION = '6.0.0' end end diff --git a/elasticsearch-persistence/spec/repository/find_spec.rb b/elasticsearch-persistence/spec/repository/find_spec.rb new file mode 100644 index 000000000..b630d45a2 --- /dev/null +++ b/elasticsearch-persistence/spec/repository/find_spec.rb @@ -0,0 +1,179 @@ +require 'spec_helper' + +describe Elasticsearch::Persistence::Repository::Find do + + after do + begin; repository.delete_index!; rescue; end + end + + let(:repository) do + DEFAULT_REPOSITORY + end + + describe '#exists?' do + + context 'when the document exists' do + + let(:id) do + repository.save(a: 1)['_id'] + end + + it 'returns true' do + expect(repository.exists?(id)).to be(true) + end + end + + context 'when the document does not exist' do + + it 'returns false' do + expect(repository.exists?('1')).to be(false) + end + end + + context 'when options are provided' do + + let(:id) do + repository.save(a: 1)['_id'] + end + + it 'applies the options' do + expect(repository.exists?(id, type: 'other_type')).to be(false) + end + end + end + + describe '#find' do + + context 'when options are not provided' do + + context 'when a single id is provided' do + + let!(:id) do + repository.save(a: 1)['_id'] + end + + it 'retrieves the document' do + expect(repository.find(id)).to eq('a' => 1) + end + end + + context 'when an array of ids is provided' do + + let!(:ids) do + 3.times.collect do |i| + repository.save(a: i)['_id'] + end + end + + it 'retrieves the documents' do + expect(repository.find(ids)).to eq([{ 'a' =>0 }, + { 'a' => 1 }, + { 'a' => 2 }]) + end + + context 'when some documents are found and some are not' do + + before do + ids[1] = 22 + ids + end + + it 'returns nil in the result list for the documents not found' do + expect(repository.find(ids)).to eq([{ 'a' =>0 }, + nil, + { 'a' => 2 }]) + end + end + end + + context 'when multiple ids are provided' do + + let!(:ids) do + 3.times.collect do |i| + repository.save(a: i)['_id'] + end + end + + it 'retrieves the documents' do + expect(repository.find(*ids)).to eq([{ 'a' =>0 }, + { 'a' => 1 }, + { 'a' => 2 }]) + end + end + + context 'when the document cannot be found' do + + before do + begin; repository.create_index!; rescue; end + end + + it 'raises a DocumentNotFound exception' do + expect { + repository.find(1) + }.to raise_exception(Elasticsearch::Persistence::Repository::DocumentNotFound) + end + end + end + + context 'when options are provided' do + + context 'when a single id is passed' do + + let!(:id) do + repository.save(a: 1)['_id'] + end + + it 'applies the options' do + expect { + repository.find(id, type: 'none') + }.to raise_exception(Elasticsearch::Persistence::Repository::DocumentNotFound) + end + end + + context 'when an array of ids is passed' do + + let!(:ids) do + 3.times.collect do |i| + repository.save(a: i)['_id'] + end + end + + it 'applies the options' do + expect(repository.find(ids, type: 'none')).to eq([nil, nil, nil]) + end + end + + context 'when multiple ids are passed' do + + let!(:ids) do + 3.times.collect do |i| + repository.save(a: i)['_id'] + end + end + + it 'applies the options' do + expect(repository.find(*ids, type: 'none')).to eq([nil, nil, nil]) + end + end + end + + context 'when a document_type is defined on the class' do + + let(:repository) do + MyTestRepository.new(document_type:'other_type', client: DEFAULT_CLIENT, index_name: 'test') + end + + let!(:ids) do + 3.times.collect do |i| + repository.save(a: i)['_id'] + end + end + + it 'uses the document type in the query' do + expect(repository.find(ids)).to eq([{ 'a' =>0 }, + { 'a' => 1 }, + { 'a' => 2 }]) + end + end + end +end diff --git a/elasticsearch-persistence/spec/repository/search_spec.rb b/elasticsearch-persistence/spec/repository/search_spec.rb new file mode 100644 index 000000000..939f09731 --- /dev/null +++ b/elasticsearch-persistence/spec/repository/search_spec.rb @@ -0,0 +1,181 @@ +require 'spec_helper' + +describe Elasticsearch::Persistence::Repository::Search do + + after do + begin; repository.delete_index!; rescue; end + end + + describe '#search' do + + let(:repository) do + DEFAULT_REPOSITORY + end + + context 'when the repository does not have a type set' do + + before do + repository.save({ name: 'user' }, refresh: true) + end + + context 'when a query definition is provided as a hash' do + + it 'uses the default document type' do + expect(repository.search({ query: { match: { name: 'user' } } }).first).to eq('name' => 'user') + end + end + + context 'when a query definition is provided as a string' do + + it 'uses the default document type' do + expect(repository.search('user').first).to eq('name' => 'user') + end + end + + context 'when the query definition is neither a String nor a Hash' do + + it 'raises an ArgumentError' do + expect { + repository.search(1) + }.to raise_exception(ArgumentError) + end + end + + context 'when options are provided' do + + context 'when a query definition is provided as a hash' do + + it 'uses the default document type' do + expect(repository.search({ query: { match: { name: 'user' } } }, type: 'other').first).to be_nil + end + end + + context 'when a query definition is provided as a string' do + + it 'uses the default document type' do + expect(repository.search('user', type: 'other').first).to be_nil + end + end + + context 'when the query definition is neither a String nor a Hash' do + + it 'raises an ArgumentError' do + expect { + repository.search(1) + }.to raise_exception(ArgumentError) + end + end + end + end + + context 'when the repository does have a type set' do + + let(:repository) do + MyTestRepository.new(document_type: 'other_note') + end + + before do + repository.save({ name: 'user' }, refresh: true) + end + + context 'when options are provided' do + + context 'when a query definition is provided as a hash' do + + it 'uses the options' do + expect(repository.search({ query: { match: { name: 'user' } } }, type: 'other').first).to be_nil + end + end + + context 'when a query definition is provided as a string' do + + it 'uses the options' do + expect(repository.search('user', type: 'other').first).to be_nil + end + end + + context 'when the query definition is neither a String nor a Hash' do + + it 'raises an ArgumentError' do + expect { + repository.search(1) + }.to raise_exception(ArgumentError) + end + end + end + end + end + + describe '#count' do + + context 'when the repository does not have a type set' do + + let(:repository) do + DEFAULT_REPOSITORY + end + + before do + repository.save({ name: 'user' }, refresh: true) + end + + context 'when a query definition is provided as a hash' do + + it 'uses the default document type' do + expect(repository.count({ query: { match: { name: 'user' } } })).to eq(1) + end + end + + context 'when a query definition is provided as a string' do + + it 'uses the default document type' do + expect(repository.count('user')).to eq(1) + end + end + + context 'when options are provided' do + + context 'when a query definition is provided as a hash' do + + it 'uses the options' do + expect(repository.count({ query: { match: { name: 'user' } } }, type: 'other')).to eq(0) + end + end + + context 'when a query definition is provided as a string' do + + it 'uses the options' do + expect(repository.count('user', type: 'other')).to eq(0) + end + end + end + end + + context 'when the repository does have a type set' do + + let(:repository) do + MyTestRepository.new(document_type: 'other_note') + end + + before do + repository.save({ name: 'user' }, refresh: true) + end + + context 'when options are provided' do + + context 'when a query definition is provided as a hash' do + + it 'uses the options' do + expect(repository.count({ query: { match: { name: 'user' } } }, type: 'other')).to eq(0) + end + end + + context 'when a query definition is provided as a string' do + + it 'uses the options' do + expect(repository.count('user', type: 'other')).to eq(0) + end + end + end + end + end +end diff --git a/elasticsearch-persistence/spec/repository/serialize_spec.rb b/elasticsearch-persistence/spec/repository/serialize_spec.rb new file mode 100644 index 000000000..bf0d7175e --- /dev/null +++ b/elasticsearch-persistence/spec/repository/serialize_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe Elasticsearch::Persistence::Repository::Serialize do + + let(:repository) do + DEFAULT_REPOSITORY + end + + describe '#serialize' do + + before do + class MyDocument + def to_hash + { a: 1 } + end + end + end + + it 'calls #to_hash on the object' do + expect(repository.serialize(MyDocument.new)).to eq(a: 1) + end + end + + describe '#deserialize' do + + context 'when klass is defined on the Repository' do + + let(:repository) do + require 'set' + MyTestRepository.new(klass: Set) + end + + it 'instantiates an object of the klass' do + expect(repository.deserialize('_source' => { a: 1 })).to be_a(Set) + end + + it 'uses the source field to instantiate the object' do + expect(repository.deserialize('_source' => { a: 1 })).to eq(Set.new({ a: 1})) + end + end + + context 'when klass is not defined on the Repository' do + + it 'returns the raw Hash' do + expect(repository.deserialize('_source' => { a: 1 })).to be_a(Hash) + end + + it 'uses the source field to instantiate the object' do + expect(repository.deserialize('_source' => { a: 1 })).to eq(a: 1) + end + end + end +end diff --git a/elasticsearch-persistence/spec/repository/store_spec.rb b/elasticsearch-persistence/spec/repository/store_spec.rb new file mode 100644 index 000000000..c42c811d2 --- /dev/null +++ b/elasticsearch-persistence/spec/repository/store_spec.rb @@ -0,0 +1,327 @@ +require 'spec_helper' + +describe Elasticsearch::Persistence::Repository::Store do + + let(:repository) do + DEFAULT_REPOSITORY + end + + after do + begin; repository.delete_index!; rescue; end + end + + describe '#save' do + + let(:document) do + { a: 1 } + end + + let(:response) do + repository.save(document) + end + + it 'saves the document' do + expect(repository.find(response['_id'])).to eq('a' => 1) + end + + context 'when the repository defines a custom serialize method' do + + before do + class OtherNoteRepository + include Elasticsearch::Persistence::Repository + def serialize(document) + { b: 1 } + end + end + end + + after do + if defined?(OtherNoteRepository) + Object.send(:remove_const, OtherNoteRepository.name) + end + end + + let(:repository) do + OtherNoteRepository.new(client: DEFAULT_CLIENT) + end + + let(:response) do + repository.save(document) + end + + it 'saves the document' do + expect(repository.find(response['_id'])).to eq('b' => 1) + end + end + + context 'when options are provided' do + + let!(:response) do + repository.save(document, type: 'other_note') + end + + it 'saves the document using the options' do + expect { + repository.find(response['_id']) + }.to raise_exception(Elasticsearch::Persistence::Repository::DocumentNotFound) + expect(repository.find(response['_id'], type: 'other_note')).to eq('a' => 1) + end + end + end + + describe '#update' do + + before(:all) do + class Note + def to_hash + { text: 'testing', views: 0 } + end + end + end + + after(:all) do + if defined?(Note) + Object.send(:remove_const, :Note) + end + end + + context 'when the document exists' do + + let!(:id) do + repository.save(Note.new)['_id'] + end + + context 'when an id is provided' do + + context 'when a doc is specified in the options' do + + before do + repository.update(id, doc: { text: 'testing_2' }) + end + + it 'updates using the doc parameter' do + expect(repository.find(id)).to eq('text' => 'testing_2', 'views' => 0) + end + end + + context 'when a script is specified in the options' do + + before do + repository.update(id, script: { inline: 'ctx._source.views += 1' }) + end + + it 'updates using the script parameter' do + expect(repository.find(id)).to eq('text' => 'testing', 'views' => 1) + end + end + + context 'when params are specified in the options' do + + before do + repository.update(id, script: { inline: 'ctx._source.views += params.count', + params: { count: 2 } }) + end + + it 'updates using the script parameter' do + expect(repository.find(id)).to eq('text' => 'testing', 'views' => 2) + end + end + + context 'when upsert is specified in the options' do + + before do + repository.update(id, script: { inline: 'ctx._source.views += 1' }, + upsert: { text: 'testing_2' }) + end + + it 'executes the script' do + expect(repository.find(id)).to eq('text' => 'testing', 'views' => 1) + end + end + + context 'when doc_as_upsert is specified in the options' do + + before do + repository.update(id, doc: { text: 'testing_2' }, + doc_as_upsert: true) + end + + it 'performs an upsert' do + expect(repository.find(id)).to eq('text' => 'testing_2', 'views' => 0) + end + end + end + + context 'when a document is provided as the query criteria' do + + context 'when no options are provided' do + + before do + repository.update(id: id, text: 'testing_2') + end + + it 'updates using the id and the document as the doc parameter' do + expect(repository.find(id)).to eq('text' => 'testing_2', 'views' => 0) + end + end + + context 'when options are provided' do + + context 'when a doc is specified in the options' do + + before do + repository.update({ id: id, text: 'testing' }, doc: { text: 'testing_2' }) + end + + it 'updates using the id and the doc in the options' do + expect(repository.find(id)).to eq('text' => 'testing_2', 'views' => 0) + end + end + + context 'when a script is specified in the options' do + + before do + repository.update({ id: id, text: 'testing' }, + script: { inline: 'ctx._source.views += 1' }) + end + + it 'updates using the id and script from the options' do + expect(repository.find(id)).to eq('text' => 'testing', 'views' => 1) + end + end + + context 'when params are specified in the options' do + + before do + repository.update({ id: id, text: 'testing' }, + script: { inline: 'ctx._source.views += params.count', + params: { count: 2 } }) + end + + it 'updates using the id and script and params from the options' do + expect(repository.find(id)).to eq('text' => 'testing', 'views' => 2) + end + end + + context 'when upsert is specified in the options' do + + before do + repository.update({ id: id, text: 'testing_2' }, + doc_as_upsert: true) + end + + it 'updates using the id and script and params from the options' do + expect(repository.find(id)).to eq('text' => 'testing_2', 'views' => 0) + end + end + end + end + end + + context 'when the document does not exist' do + + context 'when an id is provided 'do + + it 'raises an exception' do + expect { + repository.update(1, doc: { text: 'testing_2' }) + }.to raise_exception(Elasticsearch::Transport::Transport::Errors::NotFound) + end + + context 'when upsert is provided' do + + before do + repository.update(1, doc: { text: 'testing' }, doc_as_upsert: true) + end + + it 'upserts the document' do + expect(repository.find(1)).to eq('text' => 'testing') + end + end + end + + context 'when a document is provided' do + + it 'raises an exception' do + expect { + repository.update(id: 1, text: 'testing_2') + }.to raise_exception(Elasticsearch::Transport::Transport::Errors::NotFound) + end + + context 'when upsert is provided' do + + before do + repository.update({ id: 1, text: 'testing' }, doc_as_upsert: true) + end + + it 'upserts the document' do + expect(repository.find(1)).to eq('text' => 'testing') + end + end + end + end + end + + describe '#delete' do + + before(:all) do + class Note + def to_hash + { text: 'testing', views: 0 } + end + end + end + + after(:all) do + if defined?(Note) + Object.send(:remove_const, :Note) + end + end + + context 'when the document exists' do + + let!(:id) do + repository.save(Note.new)['_id'] + end + + context 'an id is provided' do + + before do + repository.delete(id) + end + + it 'deletes the document using the id' do + expect { + repository.find(id) + }.to raise_exception(Elasticsearch::Persistence::Repository::DocumentNotFound) + end + end + + context 'when a document is provided' do + + before do + repository.delete(id: id, text: 'testing') + end + + it 'deletes the document using the document' do + expect { + repository.find(id) + }.to raise_exception(Elasticsearch::Persistence::Repository::DocumentNotFound) + end + end + end + + context 'when the document does not exist' do + + before do + repository.create_index! + end + + it 'raises an exception' do + expect { + repository.delete(1) + }.to raise_exception(Elasticsearch::Transport::Transport::Errors::NotFound) + end + end + end +end diff --git a/elasticsearch-persistence/spec/repository_spec.rb b/elasticsearch-persistence/spec/repository_spec.rb new file mode 100644 index 000000000..2d2d4d4ca --- /dev/null +++ b/elasticsearch-persistence/spec/repository_spec.rb @@ -0,0 +1,716 @@ +require 'spec_helper' + +describe Elasticsearch::Persistence::Repository do + + describe '#create' do + + before(:all) do + class RepositoryWithoutDSL + include Elasticsearch::Persistence::Repository + end + end + + after(:all) do + if defined?(RepositoryWithoutDSL) + Object.send(:remove_const, RepositoryWithoutDSL.name) + end + end + + it 'creates a repository object' do + expect(RepositoryWithoutDSL.create).to be_a(RepositoryWithoutDSL) + end + + context 'when options are provided' do + + let(:repository) do + RepositoryWithoutDSL.create(document_type: 'note') + end + + it 'sets the options on the instance' do + expect(repository.document_type).to eq('note') + end + end + + context 'when a block is passed' do + + let(:repository) do + RepositoryWithoutDSL.create(document_type: 'note') do + mapping dynamic: 'strict' do + indexes :foo + end + end + end + + it 'executes the block on the instance' do + expect(repository.mapping.to_hash).to eq(note: { dynamic: 'strict', properties: { foo: { type: 'text' } } }) + end + + context 'when options are provided in the args and set in the block' do + + let(:repository) do + RepositoryWithoutDSL.create(mapping: double('mapping', to_hash: {}), document_type: 'note') do + mapping dynamic: 'strict' do + indexes :foo + end + end + end + + it 'uses the options from the args' do + expect(repository.mapping.to_hash).to eq({}) + end + end + end + end + + describe '#initialize' do + + before(:all) do + class RepositoryWithoutDSL + include Elasticsearch::Persistence::Repository + end + end + + after(:all) do + if defined?(RepositoryWithoutDSL) + Object.send(:remove_const, RepositoryWithoutDSL.name) + end + end + + after do + begin; repository.delete_index!; rescue; end + end + + context 'when options are not provided' do + + let(:repository) do + RepositoryWithoutDSL.new + end + + it 'sets a default client' do + expect(repository.client).to be_a(Elasticsearch::Transport::Client) + end + + it 'sets a default document type' do + expect(repository.document_type).to eq('_doc') + end + + it 'sets a default index name' do + expect(repository.index_name).to eq('repository') + end + + it 'does not set a klass' do + expect(repository.klass).to be_nil + end + end + + context 'when options are provided' do + + let(:client) do + Elasticsearch::Transport::Client.new + end + + let(:repository) do + RepositoryWithoutDSL.new(client: client, document_type: 'user', index_name: 'users', klass: Array) + end + + it 'sets the client' do + expect(repository.client).to be(client) + end + + it 'sets document type' do + expect(repository.document_type).to eq('user') + end + + it 'sets index name' do + expect(repository.index_name).to eq('users') + end + + it 'sets the klass' do + expect(repository.klass).to eq(Array) + end + end + end + + context 'when the DSL module is included' do + + before(:all) do + class RepositoryWithDSL + include Elasticsearch::Persistence::Repository + include Elasticsearch::Persistence::Repository::DSL + + document_type 'note' + index_name 'notes_repo' + klass Hash + client DEFAULT_CLIENT + + settings number_of_shards: 1, number_of_replicas: 0 do + mapping dynamic: 'strict' do + indexes :foo do + indexes :bar + end + indexes :baz + end + end + end + end + + after(:all) do + if defined?(RepositoryWithDSL) + Object.send(:remove_const, RepositoryWithDSL.name) + end + end + + after do + begin; repository.delete_index; rescue; end + end + + context '#client' do + + it 'allows the value to be set only once on the class' do + RepositoryWithDSL.client(double('client', class: 'other_client')) + expect(RepositoryWithDSL.client).to be(DEFAULT_CLIENT) + end + + it 'sets the value at the class level' do + expect(RepositoryWithDSL.client).to be(DEFAULT_CLIENT) + end + + it 'sets the value as the default at the instance level' do + expect(RepositoryWithDSL.new.client).to be(DEFAULT_CLIENT) + end + + it 'allows the value to be overridden with options on the instance' do + expect(RepositoryWithDSL.new(client: double('client', instance: 'other')).client.instance).to eq('other') + end + end + + context '#klass' do + + it 'allows the value to be set only once on the class' do + RepositoryWithDSL.klass(Array) + expect(RepositoryWithDSL.klass).to eq(Hash) + end + + it 'sets the value at the class level' do + expect(RepositoryWithDSL.klass).to eq(Hash) + end + + it 'sets the value as the default at the instance level' do + expect(RepositoryWithDSL.new.klass).to eq(Hash) + end + + it 'allows the value to be overridden with options on the instance' do + expect(RepositoryWithDSL.new(klass: Array).klass).to eq(Array) + end + + context 'when nil is passed to the method' do + + before do + RepositoryWithDSL.klass(nil) + end + + it 'allows the value to be set only once' do + expect(RepositoryWithDSL.klass).to eq(Hash) + end + end + end + + context '#document_type' do + + it 'allows the value to be set only once on the class' do + RepositoryWithDSL.document_type('other_note') + expect(RepositoryWithDSL.document_type).to eq('note') + end + + it 'sets the value at the class level' do + expect(RepositoryWithDSL.document_type).to eq('note') + end + + it 'sets the value as the default at the instance level' do + expect(RepositoryWithDSL.new.document_type).to eq('note') + end + + it 'allows the value to be overridden with options on the instance' do + expect(RepositoryWithDSL.new(document_type: 'other_note').document_type).to eq('other_note') + end + end + + context '#index_name' do + + it 'allows the value to be set only once on the class' do + RepositoryWithDSL.index_name('other_name') + expect(RepositoryWithDSL.index_name).to eq('notes_repo') + end + + it 'sets the value at the class level' do + expect(RepositoryWithDSL.index_name).to eq('notes_repo') + end + + it 'sets the value as the default at the instance level' do + expect(RepositoryWithDSL.new.index_name).to eq('notes_repo') + end + + it 'allows the value to be overridden with options on the instance' do + expect(RepositoryWithDSL.new(index_name: 'other_notes_repo').index_name).to eq('other_notes_repo') + end + end + + describe '#create_index!' do + + context 'when the method is called on an instance' do + + let(:repository) do + RepositoryWithDSL.new + end + + before do + begin; repository.delete_index!; rescue; end + repository.create_index! + end + + it 'creates the index' do + expect(repository.index_exists?).to be(true) + end + end + + context 'when the method is called on the class' do + + it 'raises a NotImplementedError' do + expect { + RepositoryWithDSL.create_index! + }.to raise_exception(NotImplementedError) + end + end + end + + describe '#delete_index!' do + + context 'when the method is called on an instance' do + + let(:repository) do + RepositoryWithDSL.new + end + + before do + repository.create_index! + begin; repository.delete_index!; rescue; end + end + + it 'deletes the index' do + expect(repository.index_exists?).to be(false) + end + end + + context 'when the method is called on the class' do + + it 'raises a NotImplementedError' do + expect { + RepositoryWithDSL.delete_index! + }.to raise_exception(NotImplementedError) + end + end + end + + describe '#refresh_index!' do + + context 'when the method is called on an instance' do + + let(:repository) do + RepositoryWithDSL.new + end + + before do + repository.create_index! + end + + it 'refreshes the index' do + expect(repository.refresh_index!['_shards']).to be_a(Hash) + end + end + + context 'when the method is called on the class' do + + it 'raises a NotImplementedError' do + expect { + RepositoryWithDSL.refresh_index! + }.to raise_exception(NotImplementedError) + end + end + end + + describe '#index_exists?' do + + context 'when the method is called on an instance' do + + let(:repository) do + RepositoryWithDSL.new + end + + before do + repository.create_index! + end + + it 'determines if the index exists' do + expect(repository.index_exists?).to be(true) + end + end + + context 'when the method is called on the class' do + + it 'raises a NotImplementedError' do + expect { + RepositoryWithDSL.index_exists? + }.to raise_exception(NotImplementedError) + end + end + end + + describe '#mapping' do + + let(:expected_mapping) do + { note: { dynamic: 'strict', + properties: { foo: { type: 'object', + properties: { bar: { type: 'text' } } }, + baz: { type: 'text' } } + } + } + end + + it 'sets the value at the class level' do + expect(RepositoryWithDSL.mapping.to_hash).to eq(expected_mapping) + end + + it 'sets the value as the default at the instance level' do + expect(RepositoryWithDSL.new.mapping.to_hash).to eq(expected_mapping) + end + + it 'allows the value to be overridden with options on the instance' do + expect(RepositoryWithDSL.new(mapping: double('mapping', to_hash: { note: {} })).mapping.to_hash).to eq(note: {}) + end + + context 'when the instance has a different document type' do + + let(:expected_mapping) do + { other_note: { dynamic: 'strict', + properties: { foo: { type: 'object', + properties: { bar: { type: 'text' } } }, + baz: { type: 'text' } } + } + } + end + + it 'updates the mapping to use the document type' do + expect(RepositoryWithDSL.new(document_type: 'other_note').mapping.to_hash).to eq(expected_mapping) + end + end + end + + describe '#settings' do + + it 'sets the value at the class level' do + expect(RepositoryWithDSL.settings.to_hash).to eq(number_of_shards: 1, number_of_replicas: 0) + end + + it 'sets the value as the default at the instance level' do + expect(RepositoryWithDSL.new.settings.to_hash).to eq(number_of_shards: 1, number_of_replicas: 0) + end + + it 'allows the value to be overridden with options on the instance' do + expect(RepositoryWithDSL.new(settings: { number_of_shards: 3 }).settings.to_hash).to eq({number_of_shards: 3}) + end + end + end + + context 'when the DSL module is not included' do + + before(:all) do + class RepositoryWithoutDSL + include Elasticsearch::Persistence::Repository + end + end + + after(:all) do + if defined?(RepositoryWithoutDSL) + Object.send(:remove_const, RepositoryWithoutDSL.name) + end + end + + context '#client' do + + it 'does not define the method at the class level' do + expect { + RepositoryWithoutDSL.client + }.to raise_exception(NoMethodError) + end + + it 'sets a default on the instance' do + expect(RepositoryWithoutDSL.new.client).to be_a(Elasticsearch::Transport::Client) + end + + it 'allows the value to be overridden with options on the instance' do + expect(RepositoryWithoutDSL.new(client: double('client', object_id: 123)).client.object_id).to eq(123) + end + end + + context '#klass' do + + it 'does not define the method at the class level' do + expect { + RepositoryWithoutDSL.klass + }.to raise_exception(NoMethodError) + end + + it 'does not set a default on an instance' do + expect(RepositoryWithoutDSL.new.klass).to be_nil + end + + it 'allows the value to be overridden with options on the instance' do + expect(RepositoryWithoutDSL.new(klass: Array).klass).to eq(Array) + end + end + + context '#document_type' do + + it 'does not define the method at the class level' do + expect { + RepositoryWithoutDSL.document_type + }.to raise_exception(NoMethodError) + end + + it 'sets a default on the instance' do + expect(RepositoryWithoutDSL.new.document_type).to eq('_doc') + end + + it 'allows the value to be overridden with options on the instance' do + expect(RepositoryWithoutDSL.new(document_type: 'notes').document_type).to eq('notes') + end + end + + context '#index_name' do + + it 'does not define the method at the class level' do + expect { + RepositoryWithoutDSL.index_name + }.to raise_exception(NoMethodError) + end + + it 'sets a default on the instance' do + expect(RepositoryWithoutDSL.new.index_name).to eq('repository') + end + + it 'allows the value to be overridden with options on the instance' do + expect(RepositoryWithoutDSL.new(index_name: 'notes_repository').index_name).to eq('notes_repository') + end + end + + describe '#create_index!' do + + let(:repository) do + RepositoryWithoutDSL.new(client: DEFAULT_CLIENT) + end + + after do + begin; repository.delete_index!; rescue; end + end + + it 'does not define the method at the class level' do + expect { + RepositoryWithoutDSL.create_index! + }.to raise_exception(NoMethodError) + end + + it 'creates an index' do + repository.create_index! + expect(repository.index_exists?).to eq(true) + end + end + + describe '#delete_index!' do + + let(:repository) do + RepositoryWithoutDSL.new(client: DEFAULT_CLIENT) + end + + it 'does not define the method at the class level' do + expect { + RepositoryWithoutDSL.delete_index! + }.to raise_exception(NoMethodError) + end + + it 'deletes an index' do + repository.create_index! + repository.delete_index! + expect(repository.index_exists?).to eq(false) + end + end + + describe '#refresh_index!' do + + let(:repository) do + RepositoryWithoutDSL.new(client: DEFAULT_CLIENT) + end + + after do + begin; repository.delete_index!; rescue; end + end + + it 'does not define the method at the class level' do + expect { + RepositoryWithoutDSL.refresh_index! + }.to raise_exception(NoMethodError) + end + + it 'refreshes an index' do + repository.create_index! + expect(repository.refresh_index!['_shards']).to be_a(Hash) + end + end + + describe '#index_exists?' do + + let(:repository) do + RepositoryWithoutDSL.new(client: DEFAULT_CLIENT) + end + + after do + begin; repository.delete_index!; rescue; end + end + + it 'does not define the method at the class level' do + expect { + RepositoryWithoutDSL.index_exists? + }.to raise_exception(NoMethodError) + end + + it 'returns whether the index exists' do + repository.create_index! + expect(repository.index_exists?).to be(true) + end + end + + describe '#mapping' do + + it 'does not define the method at the class level' do + expect { + RepositoryWithoutDSL.mapping + }.to raise_exception(NoMethodError) + end + + it 'sets a default on an instance' do + expect(RepositoryWithoutDSL.new.mapping.to_hash).to eq(_doc: { properties: {} }) + end + + it 'allows the mapping to be set as an option' do + expect(RepositoryWithoutDSL.new(mapping: double('mapping', to_hash: { note: {} })).mapping.to_hash).to eq(note: {}) + end + + context 'when a block is passed to the create method' do + + let(:expected_mapping) do + { note: { dynamic: 'strict', + properties: { foo: { type: 'object', + properties: { bar: { type: 'text' } } }, + baz: { type: 'text' } } + } + } + end + + let(:repository) do + RepositoryWithoutDSL.create(document_type: 'note') do + mapping dynamic: 'strict' do + indexes :foo do + indexes :bar + end + indexes :baz + end + end + end + + it 'allows the mapping to be set in the block' do + expect(repository.mapping.to_hash).to eq(expected_mapping) + end + + context 'when the mapping is set in the options' do + + let(:repository) do + RepositoryWithoutDSL.create(mapping: double('mapping', to_hash: { note: {} })) do + mapping dynamic: 'strict' do + indexes :foo do + indexes :bar + end + indexes :baz + end + end + end + + it 'uses the mapping from the options' do + expect(repository.mapping.to_hash).to eq(note: {}) + end + end + end + end + + describe '#settings' do + + it 'does not define the method at the class level' do + expect { + RepositoryWithoutDSL.settings + }.to raise_exception(NoMethodError) + end + + it 'sets a default on an instance' do + expect(RepositoryWithoutDSL.new.settings.to_hash).to eq({}) + end + + it 'allows the settings to be set as an option' do + expect(RepositoryWithoutDSL.new(settings: double('settings', to_hash: {})).settings.to_hash).to eq({}) + end + + context 'when a block is passed to the #create method' do + + let(:repository) do + RepositoryWithoutDSL.create(document_type: 'note') do + settings number_of_shards: 1, number_of_replicas: 0 + end + end + + it 'allows the settings to be set with a block' do + expect(repository.settings.to_hash).to eq(number_of_shards: 1, number_of_replicas: 0) + end + + context 'when a mapping is set in the block as well' do + + let(:expected_mapping) do + { note: { dynamic: 'strict', + properties: { foo: { type: 'object', + properties: { bar: { type: 'text' } } }, + baz: { type: 'text' } } + } + } + end + + let(:repository) do + RepositoryWithoutDSL.create(document_type: 'note') do + settings number_of_shards: 1, number_of_replicas: 0 do + mapping dynamic: 'strict' do + indexes :foo do + indexes :bar + end + indexes :baz + end + end + end + end + + it 'allows the settings to be set with a block' do + expect(repository.settings.to_hash).to eq(number_of_shards: 1, number_of_replicas: 0) + end + + it 'allows the mapping to be set with a block' do + expect(repository.mappings.to_hash).to eq(expected_mapping) + end + end + end + end + end +end diff --git a/elasticsearch-persistence/spec/spec_helper.rb b/elasticsearch-persistence/spec/spec_helper.rb new file mode 100644 index 000000000..9f2f0f5df --- /dev/null +++ b/elasticsearch-persistence/spec/spec_helper.rb @@ -0,0 +1,28 @@ +require 'pry-nav' +require 'elasticsearch/persistence' + +RSpec.configure do |config| + config.formatter = 'documentation' + config.color = true + + config.after(:suite) do + DEFAULT_CLIENT.indices.delete(index: '_all') + end +end + +# The default client to be used by the repositories. +# +# @since 6.0.0 +DEFAULT_CLIENT = Elasticsearch::Client.new(host: "localhost:#{(ENV['TEST_CLUSTER_PORT'] || 9250)}", + tracer: (ENV['QUIET'] ? nil : ::Logger.new(STDERR))) + +class MyTestRepository + include Elasticsearch::Persistence::Repository + include Elasticsearch::Persistence::Repository::DSL + client DEFAULT_CLIENT +end + +# The default repository to be used by tests. +# +# @since 6.0.0 +DEFAULT_REPOSITORY = MyTestRepository.new(index_name: 'my_test_repository', document_type: 'test') diff --git a/elasticsearch-persistence/test/integration/repository/custom_class_test.rb b/elasticsearch-persistence/test/integration/repository/custom_class_test.rb deleted file mode 100644 index 6528dd438..000000000 --- a/elasticsearch-persistence/test/integration/repository/custom_class_test.rb +++ /dev/null @@ -1,88 +0,0 @@ -require 'test_helper' - -module Elasticsearch - module Persistence - class RepositoryCustomClassIntegrationTest < Elasticsearch::Test::IntegrationTestCase - - class ::MyNote - attr_reader :attributes - - def initialize(attributes={}) - @attributes = Hashie::Mash.new(attributes) - end - - def method_missing(method_name, *arguments, &block) - attributes.respond_to?(method_name) ? attributes.__send__(method_name, *arguments, &block) : super - end - - def respond_to?(method_name, include_private=false) - attributes.respond_to?(method_name) || super - end - - def to_hash - @attributes - end - end - - context "A custom repository class" do - setup do - class ::MyNotesRepository - include Elasticsearch::Persistence::Repository - - klass MyNote - document_type 'my_note' - - settings number_of_shards: 1 do - mapping do - indexes :title, analyzer: 'snowball' - end - end - - create_index! - - def deserialize(document) - klass.new document.merge(document['_source']) - end - end - - @repository = MyNotesRepository.new - @repository.klass = MyNotesRepository.klass - @repository.document_type = MyNotesRepository.document_type - - @repository.client.cluster.health wait_for_status: 'yellow' - end - - should "save the object under a correct index and type" do - @repository.save MyNote.new(id: '1', title: 'Test') - result = @repository.find(1) - - assert_instance_of MyNote, result - assert_equal 'Test', result.title - - assert_not_nil Elasticsearch::Persistence.client.get index: 'my_notes_repository', - type: 'my_note', - id: '1' - end - - should "delete the object" do - note = MyNote.new id: 1, title: 'Test' - @repository.save note - - assert_not_nil @repository.find(1) - - @repository.delete(note) - assert_raise(Elasticsearch::Persistence::Repository::DocumentNotFound) { @repository.find(1) } - end - - should "retrieve the object via a search query" do - note = MyNote.new title: 'Testing' - @repository.save note, refresh: true - - results = @repository.search query: { match: { title: 'Test' } } - assert_equal 'Testing', results.first.title - end - end - - end - end -end diff --git a/elasticsearch-persistence/test/integration/repository/customized_class_test.rb b/elasticsearch-persistence/test/integration/repository/customized_class_test.rb deleted file mode 100644 index 06289b12d..000000000 --- a/elasticsearch-persistence/test/integration/repository/customized_class_test.rb +++ /dev/null @@ -1,82 +0,0 @@ -require 'test_helper' - -module Elasticsearch - module Persistence - class RepositoryCustomizedClassIntegrationTest < Elasticsearch::Test::IntegrationTestCase - - module ::My - class Note - attr_reader :attributes - - def initialize(attributes={}) - @attributes = attributes - end - - def to_hash - @attributes - end - end - end - - context "A custom repository class" do - setup do - @repository = Elasticsearch::Persistence::Repository.new do - index 'my_notes' - type 'my_note' - klass My::Note - - settings number_of_shards: 1 do - mapping do - indexes :title, analyzer: 'snowball' - end - end - - create_index! - end - - @repository.client.cluster.health wait_for_status: 'yellow' - end - - should "save the object under a correct index and type" do - @repository.save My::Note.new(id: '1', title: 'Test') - - assert_instance_of My::Note, @repository.find(1) - assert_not_nil Elasticsearch::Persistence.client.get index: 'my_notes', type: 'my_note', id: '1' - end - - should "update the document" do - @repository.save Note.new(id: 1, title: 'Test') - - @repository.update 1, doc: { title: 'UPDATED' } - assert_equal 'UPDATED', @repository.find(1).attributes['title'] - end - - should "update the document with a script" do - @repository.save Note.new(id: 1, title: 'Test') - - @repository.update 1, script: 'ctx._source.title = "UPDATED"' - assert_equal 'UPDATED', @repository.find(1).attributes['title'] - end - - should "delete the object" do - note = My::Note.new id: 1, title: 'Test' - @repository.save note - - assert_not_nil @repository.find(1) - - @repository.delete(note) - assert_raise(Elasticsearch::Persistence::Repository::DocumentNotFound) { @repository.find(1) } - end - - should "create the index with correct mapping" do - note = My::Note.new title: 'Testing' - @repository.save note, refresh: true - - results = @repository.search query: { match: { title: 'Test' } } - assert_equal 'Testing', results.first.attributes['title'] - end - end - - end - end -end diff --git a/elasticsearch-persistence/test/integration/repository/default_class_test.rb b/elasticsearch-persistence/test/integration/repository/default_class_test.rb deleted file mode 100644 index 1674a3a42..000000000 --- a/elasticsearch-persistence/test/integration/repository/default_class_test.rb +++ /dev/null @@ -1,119 +0,0 @@ -require 'test_helper' - -module Elasticsearch - module Persistence - class RepositoryDefaultClassIntegrationTest < Elasticsearch::Test::IntegrationTestCase - - class ::Note - attr_reader :attributes - - def initialize(attributes={}) - @attributes = attributes - end - - def to_hash - @attributes - end - end - - context "The default repository class" do - setup do - @repository = Elasticsearch::Persistence::Repository.new - @repository.klass = ::Note - @repository.document_type = 'note' - @repository.client.cluster.health wait_for_status: 'yellow' - end - - should "save the object with a custom ID and find it" do - @repository.save Note.new(id: '1', title: 'Test') - - assert_equal 'Test', @repository.find(1).attributes['title'] - end - - should "save the object with an auto-generated ID and find it" do - response = @repository.save Note.new(title: 'Test') - - assert_equal 'Test', @repository.find(response['_id']).attributes['title'] - end - - should "update the document" do - @repository.save Note.new(id: 1, title: 'Test') - - @repository.update 1, type: 'note', doc: { title: 'UPDATED' } - assert_equal 'UPDATED', @repository.find(1).attributes['title'] - end - - should "update the document with a script" do - @repository.save Note.new(id: 1, title: 'Test') - - @repository.update 1, type: 'note', script: 'ctx._source.title = "UPDATED"' - assert_equal 'UPDATED', @repository.find(1).attributes['title'] - end - - should "save the document with an upsert" do - @repository.update 1, type: 'note', script: 'ctx._source.clicks += 1', upsert: { clicks: 1 } - assert_equal 1, @repository.find(1).attributes['clicks'] - end - - should "delete an object" do - note = Note.new(id: '1', title: 'Test') - - @repository.save(note) - assert_not_nil @repository.find(1) - @repository.delete(note) - assert_raise(Elasticsearch::Persistence::Repository::DocumentNotFound) { @repository.find(1) } - end - - should "find multiple objects" do - (1..5).each { |i| @repository.save Note.new(id: i, title: "Test #{i}") } - - assert_equal 5, @repository.find(1, 2, 3, 4, 5).size - assert_equal 5, @repository.find([1, 2, 3, 4, 5]).size - end - - should "pass options to save and find" do - note = Note.new(id: '1', title: 'Test') - @repository.save note, routing: 'ABC' - - @repository.client.cluster.health level: 'indices', wait_for_status: 'yellow' - - assert_raise Elasticsearch::Persistence::Repository::DocumentNotFound do - @repository.find(1, routing: 'DEF') - end - - assert_nothing_raised do - note = @repository.find(1, routing: 'ABC') - assert_instance_of Note, note - end - end - - should "find notes with full text search" do - @repository.save Note.new(title: 'Test') - @repository.save Note.new(title: 'Test Test') - @repository.save Note.new(title: 'Crust') - @repository.client.indices.refresh index: @repository.index_name - - results = @repository.search 'test' - assert_equal 2, results.size - - results = @repository.search query: { match: { title: 'Test' } } - assert_equal 2, results.size - end - - should "count notes" do - @repository.save Note.new(title: 'Test') - @repository.client.indices.refresh index: @repository.index_name - assert_equal 1, @repository.count - end - - should "save and find a plain hash" do - @repository.klass = Hash - @repository.save id: 1, title: 'Hash' - result = @repository.find(1) - assert_equal 'Hash', result['_source']['title'] - end - end - - end - end -end diff --git a/elasticsearch-persistence/test/integration/repository/virtus_model_test.rb b/elasticsearch-persistence/test/integration/repository/virtus_model_test.rb deleted file mode 100644 index c6ad88177..000000000 --- a/elasticsearch-persistence/test/integration/repository/virtus_model_test.rb +++ /dev/null @@ -1,119 +0,0 @@ -require 'test_helper' - -require 'virtus' - -module Elasticsearch - module Persistence - class RepositoryWithVirtusIntegrationTest < Elasticsearch::Test::IntegrationTestCase - - class ::Page - include Virtus.model - - attribute :id, String, writer: :private - attribute :title, String - attribute :views, Integer, default: 0 - attribute :published, Boolean, default: false - attribute :slug, String, default: lambda { |page, attr| page.title.downcase.gsub(' ', '-') } - - def set_id(id) - self.id = id - end - end - - context "The repository with a Virtus model" do - setup do - @repository = Elasticsearch::Persistence::Repository.new do - index :pages - klass Page - document_type 'page' - - def deserialize(document) - page = klass.new document['_source'] - page.set_id document['_id'] - page - end - end - end - - should "save and find the object" do - page = Page.new title: 'Test Page' - - response = @repository.save page - id = response['_id'] - - result = @repository.find(id) - - assert_instance_of Page, result - assert_equal 'Test Page', result.title - assert_equal 0, result.views - - assert_not_nil Elasticsearch::Persistence.client.get index: 'pages', - type: 'page', - id: id - end - - should "update the object with a partial document" do - response = @repository.save Page.new(title: 'Test') - id = response['_id'] - - page = @repository.find(id) - - assert_equal 'Test', page.title - - @repository.update page.id, doc: { title: 'UPDATE' } - - page = @repository.find(id) - assert_equal 'UPDATE', page.title - end - - should "update the object with a Hash" do - response = @repository.save Page.new(title: 'Test') - id = response['_id'] - - page = @repository.find(id) - - assert_equal 'Test', page.title - - @repository.update id: page.id, title: 'UPDATE' - - page = @repository.find(id) - assert_equal 'UPDATE', page.title - end - - should "update the object with a script" do - response = @repository.save Page.new(title: 'Test Page') - id = response['_id'] - - page = @repository.find(id) - - assert_not_nil page.id - assert_equal 0, page.views - - @repository.update page.id, script: 'ctx._source.views += 1' - - page = @repository.find(id) - assert_equal 1, page.views - - @repository.update id: page.id, script: 'ctx._source.views += 1' - - page = @repository.find(id) - assert_equal 2, page.views - end - - should "update the object with a script and params" do - response = @repository.save Page.new(title: 'Test Page') - - @repository.update id: response['_id'], - script: { - inline: 'ctx._source.views += params.count', - params: { count: 3 } - } - - page = @repository.find(response['_id']) - assert_equal 3, page.views - end - end - - end - end -end diff --git a/elasticsearch-persistence/test/test_helper.rb b/elasticsearch-persistence/test/test_helper.rb deleted file mode 100644 index c881ac02e..000000000 --- a/elasticsearch-persistence/test/test_helper.rb +++ /dev/null @@ -1,55 +0,0 @@ -RUBY_1_8 = defined?(RUBY_VERSION) && RUBY_VERSION < '1.9' - -exit(0) if RUBY_1_8 - -$LOAD_PATH.unshift File.expand_path('../../../elasticsearch-model/lib', __FILE__) if File.exists? File.expand_path('../../../elasticsearch-model/lib', __FILE__) - -require 'simplecov' and SimpleCov.start { add_filter "/test|test_/" } if ENV["COVERAGE"] - -# Register `at_exit` handler for integration tests shutdown. -# MUST be called before requiring `test/unit`. -at_exit { Elasticsearch::Test::IntegrationTestCase.__run_at_exit_hooks } if ENV['SERVER'] - -if defined?(RUBY_VERSION) && RUBY_VERSION > '2.2' - require 'test-unit' - require 'mocha/test_unit' -else - require 'minitest/autorun' - require 'mocha/mini_test' -end - -require 'shoulda-context' - -require 'turn' unless ENV["TM_FILEPATH"] || ENV["NOTURN"] || defined?(RUBY_VERSION) && RUBY_VERSION > '2.2' - -require 'ansi' -require 'oj' unless defined?(JRUBY_VERSION) - -require 'elasticsearch/extensions/test/cluster' -require 'elasticsearch/extensions/test/startup_shutdown' - -require 'elasticsearch/persistence' - -module Elasticsearch - module Test - class IntegrationTestCase < ::Test::Unit::TestCase - extend Elasticsearch::Extensions::Test::StartupShutdown - - startup { Elasticsearch::Extensions::Test::Cluster.start(nodes: 1) if ENV['SERVER'] and not Elasticsearch::Extensions::Test::Cluster.running? } - shutdown { Elasticsearch::Extensions::Test::Cluster.stop if ENV['SERVER'] && started? } - context "IntegrationTest" do; should "noop on Ruby 1.8" do; end; end if RUBY_1_8 - - def setup - tracer = ::Logger.new(STDERR) - tracer.formatter = lambda { |s, d, p, m| "#{m.gsub(/^.*$/) { |n| ' ' + n }.ansi(:faint)}\n" } - Elasticsearch::Persistence.client = Elasticsearch::Client.new \ - host: "localhost:#{(ENV['TEST_CLUSTER_PORT'] || 9250)}", - tracer: (ENV['QUIET'] ? nil : tracer) - end - - def teardown - Elasticsearch::Persistence.client.indices.delete index: '_all' - end - end - end -end diff --git a/elasticsearch-persistence/test/unit/persistence_test.rb b/elasticsearch-persistence/test/unit/persistence_test.rb deleted file mode 100644 index cd17ba7dc..000000000 --- a/elasticsearch-persistence/test/unit/persistence_test.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'test_helper' - -class Elasticsearch::Persistence::ModuleTest < Test::Unit::TestCase - context "The Persistence module" do - - context "client" do - should "have a default client" do - client = Elasticsearch::Persistence.client - assert_not_nil client - assert_instance_of Elasticsearch::Transport::Client, client - end - - should "allow to set a client" do - begin - Elasticsearch::Persistence.client = "Foobar" - assert_equal "Foobar", Elasticsearch::Persistence.client - ensure - Elasticsearch::Persistence.client = nil - end - end - - should "allow to set a client with DSL" do - begin - Elasticsearch::Persistence.client "Foobar" - assert_equal "Foobar", Elasticsearch::Persistence.client - ensure - Elasticsearch::Persistence.client = nil - end - end - end - end -end diff --git a/elasticsearch-persistence/test/unit/repository_class_test.rb b/elasticsearch-persistence/test/unit/repository_class_test.rb deleted file mode 100644 index d29711248..000000000 --- a/elasticsearch-persistence/test/unit/repository_class_test.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'test_helper' - -class Elasticsearch::Persistence::RepositoryClassTest < Test::Unit::TestCase - context "The default repository class" do - - context "when initialized" do - should "be created from the module" do - repository = Elasticsearch::Persistence::Repository.new - assert_instance_of Elasticsearch::Persistence::Repository::Class, repository - end - - should "store and access the options" do - repository = Elasticsearch::Persistence::Repository::Class.new foo: 'bar' - assert_equal 'bar', repository.options[:foo] - end - - should "instance eval a passed block" do - $foo = 100 - repository = Elasticsearch::Persistence::Repository::Class.new() { $foo += 1 } - assert_equal 101, $foo - end - - should "call a passed block with self" do - foo = 100 - repository = Elasticsearch::Persistence::Repository::Class.new do |r| - assert_instance_of Elasticsearch::Persistence::Repository::Class, r - foo += 1 - end - assert_equal 101, foo - end - - should "configure the index name based on options" do - repository = Elasticsearch::Persistence::Repository::Class.new index: 'foobar' - assert_equal 'foobar', repository.index_name - end - end - - should "include the repository methods" do - repository = Elasticsearch::Persistence::Repository::Class.new - - %w( index_name document_type klass - mappings settings client client= - create_index! delete_index! refresh_index! - save delete serialize deserialize - exists? find search ).each do |method| - assert_respond_to repository, method - end - end - - end -end diff --git a/elasticsearch-persistence/test/unit/repository_client_test.rb b/elasticsearch-persistence/test/unit/repository_client_test.rb deleted file mode 100644 index 88e40193e..000000000 --- a/elasticsearch-persistence/test/unit/repository_client_test.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'test_helper' - -class Elasticsearch::Persistence::RepositoryClientTest < Test::Unit::TestCase - context "The repository client" do - setup do - @shoulda_subject = Class.new() { include Elasticsearch::Persistence::Repository::Client }.new - end - - should "have a default client" do - assert_not_nil subject.client - assert_instance_of Elasticsearch::Transport::Client, subject.client - end - - should "allow to set a client" do - begin - subject.client = "Foobar" - assert_equal "Foobar", subject.client - ensure - subject.client = nil - end - end - - should "allow to set the client with DSL" do - begin - subject.client "Foobar" - assert_equal "Foobar", subject.client - ensure - subject.client = nil - end - end - end -end diff --git a/elasticsearch-persistence/test/unit/repository_find_test.rb b/elasticsearch-persistence/test/unit/repository_find_test.rb deleted file mode 100644 index bcf7f8a1f..000000000 --- a/elasticsearch-persistence/test/unit/repository_find_test.rb +++ /dev/null @@ -1,307 +0,0 @@ -require 'test_helper' - -class Elasticsearch::Persistence::RepositoryFindTest < Test::Unit::TestCase - class MyDocument; end - - context "The repository" do - setup do - @shoulda_subject = Class.new() { include Elasticsearch::Persistence::Repository::Find }.new - - @client = mock - @shoulda_subject.stubs(:document_type).returns(nil) - @shoulda_subject.stubs(:klass).returns(nil) - @shoulda_subject.stubs(:index_name).returns('my_index') - @shoulda_subject.stubs(:client).returns(@client) - end - - context "find method" do - should "find one document when passed a single, literal ID" do - subject.expects(:__find_one).with(1, {}) - subject.find(1) - end - - should "find multiple documents when passed multiple IDs" do - subject.expects(:__find_many).with([1, 2], {}) - subject.find(1, 2) - end - - should "find multiple documents when passed an array of IDs" do - subject.expects(:__find_many).with([1, 2], {}) - subject.find([1, 2]) - end - - should "pass the options" do - subject.expects(:__find_one).with(1, { foo: 'bar' }) - subject.find(1, foo: 'bar') - - subject.expects(:__find_many).with([1, 2], { foo: 'bar' }) - subject.find([1, 2], foo: 'bar') - - subject.expects(:__find_many).with([1, 2], { foo: 'bar' }) - subject.find(1, 2, foo: 'bar') - end - end - - context "'exists?' method" do - should "return false when the document does not exist" do - @client.expects(:exists).returns(false) - assert_equal false, subject.exists?('1') - end - - should "return whether document for document_type exists" do - subject.expects(:document_type).twice.returns('my_document') - - @client - .expects(:exists) - .with do |arguments| - assert_equal 'my_document', arguments[:type] - assert_equal '1', arguments[:id] - true - end - .returns(true) - - assert_equal true, subject.exists?('1') - end - - should "return whether document exists using no document type" do - - @client - .expects(:exists) - .with do |arguments| - assert_equal nil, arguments[:type] - assert_equal '1', arguments[:id] - true - end - .returns(true) - - assert_equal true, subject.exists?('1') - end - - should "pass options to the client" do - @client.expects(:exists).with do |arguments| - assert_equal 'my_document', arguments[:type] - assert_equal 'foobarbam', arguments[:index] - assert_equal 'bambam', arguments[:routing] - true - end - - subject.exists? '1', index: 'foobarbam', routing: 'bambam', type: 'my_document' - end - end - - context "'__find_one' method" do - - should "find a document based on document_type and return a deserialized object" do - subject.expects(:document_type).twice.returns('my_document') - - subject.expects(:deserialize).with({'_source' => {'foo' => 'bar'}}).returns(MyDocument.new) - - @client - .expects(:get) - .with do |arguments| - assert_equal 'my_document', arguments[:type] - assert_equal '1', arguments[:id] - true - end - .returns({'_source' => { 'foo' => 'bar' }}) - - assert_instance_of MyDocument, subject.__find_one('1') - end - - should "find a document using no type if document_type is not defined" do - subject.expects(:document_type).returns(nil) - - subject.expects(:deserialize).with({'_source' => {'foo' => 'bar'}}).returns(MyDocument.new) - - @client - .expects(:get) - .with do |arguments| - assert_equal nil, arguments[:type] - assert_equal '1', arguments[:id] - true - end - .returns({'_source' => { 'foo' => 'bar' }}) - - assert_instance_of MyDocument, subject.__find_one('1') - end - - should "raise DocumentNotFound exception when the document cannot be found" do - subject.expects(:document_type).returns(nil) - - subject.expects(:deserialize).never - - @client - .expects(:get) - .raises(Elasticsearch::Transport::Transport::Errors::NotFound) - - assert_raise Elasticsearch::Persistence::Repository::DocumentNotFound do - subject.__find_one('foobar') - end - end - - should "pass other exceptions" do - subject.expects(:deserialize).never - - @client - .expects(:get) - .raises(RuntimeError) - - assert_raise RuntimeError do - subject.__find_one('foobar') - end - end - - should "pass options to the client" do - subject.expects(:deserialize) - - @client - .expects(:get) - .with do |arguments| - assert_equal 'foobarbam', arguments[:index] - assert_equal 'bambam', arguments[:routing] - true - end - .returns({'_source' => { 'foo' => 'bar' }}) - - subject.__find_one '1', index: 'foobarbam', routing: 'bambam', type: 'my_document' - end - end - - context "'__find_many' method" do - setup do - @response = {"docs"=> - [ {"_index"=>"my_index", - "_type"=>"note", - "_id"=>"1", - "_version"=>1, - "found"=>true, - "_source"=>{"id"=>"1", "title"=>"Test 1"}}, - - {"_index"=>"my_index", - "_type"=>"note", - "_id"=>"2", - "_version"=>1, - "found"=>true, - "_source"=>{"id"=>"2", "title"=>"Test 2"}} - ]} - end - - should "find documents based on document_type and return an Array of deserialized objects" do - subject.expects(:document_type).twice.returns('my_document') - - subject.expects(:deserialize).twice - - @client - .expects(:mget) - .with do |arguments| - assert_equal 'my_document', arguments[:type] - assert_equal ['1', '2'], arguments[:body][:ids] - true - end - .returns(@response) - - subject.__find_many(['1', '2']) - end - - should "find documents and return an Array of deserialized objects" do - subject.expects(:document_type).returns(nil) - - subject - .expects(:deserialize) - .with(@response['docs'][0]) - .returns(MyDocument.new) - - subject - .expects(:deserialize) - .with(@response['docs'][1]) - .returns(MyDocument.new) - - @client - .expects(:mget) - .with do |arguments| - assert_equal nil, arguments[:type] - assert_equal ['1', '2'], arguments[:body][:ids] - true - end - .returns(@response) - - results = subject.__find_many(['1', '2']) - - assert_equal 2, results.size - - assert_instance_of MyDocument, results[0] - assert_instance_of MyDocument, results[1] - end - - should "find keep missing documents in the result as nil" do - @response = {"docs"=> - [ {"_index"=>"my_index", - "_type"=>"note", - "_id"=>"1", - "_version"=>1, - "found"=>true, - "_source"=>{"id"=>"1", "title"=>"Test 1"}}, - - {"_index"=>"my_index", - "_type"=>"note", - "_id"=>"3", - "_version"=>1, - "found"=>false}, - - {"_index"=>"my_index", - "_type"=>"note", - "_id"=>"2", - "_version"=>1, - "found"=>true, - "_source"=>{"id"=>"2", "title"=>"Test 2"}} - ]} - - subject.expects(:document_type).twice.returns('my_document') - - subject - .expects(:deserialize) - .with(@response['docs'][0]) - .returns(MyDocument.new) - - subject - .expects(:deserialize) - .with(@response['docs'][2]) - .returns(MyDocument.new) - - @client - .expects(:mget) - .with do |arguments| - assert_equal 'my_document', arguments[:type] - assert_equal ['1', '3', '2'], arguments[:body][:ids] - true - end - .returns(@response) - - results = subject.__find_many(['1', '3', '2']) - - assert_equal 3, results.size - - assert_instance_of MyDocument, results[0] - assert_instance_of NilClass, results[1] - assert_instance_of MyDocument, results[2] - end - - should "pass options to the client" do - subject.expects(:deserialize).twice - - @client - .expects(:mget) - .with do |arguments| - assert_equal 'my_document', arguments[:type] - assert_equal 'foobarbam', arguments[:index] - assert_equal 'bambam', arguments[:routing] - true - end - .returns(@response) - - subject.__find_many ['1', '2'], index: 'foobarbam', routing: 'bambam', type: 'my_document' - end - end - - end -end diff --git a/elasticsearch-persistence/test/unit/repository_indexing_test.rb b/elasticsearch-persistence/test/unit/repository_indexing_test.rb deleted file mode 100644 index 51b2b8b21..000000000 --- a/elasticsearch-persistence/test/unit/repository_indexing_test.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'test_helper' - -class Elasticsearch::Persistence::RepositoryIndexingTest < Test::Unit::TestCase - context "The repository index methods" do - class MyDocument; end - - setup do - @shoulda_subject = Class.new() { include Elasticsearch::Model::Indexing::ClassMethods }.new - @shoulda_subject.stubs(:index_name).returns('my_index') - @shoulda_subject.stubs(:document_type).returns('my_document') - end - - should "have the convenience index management methods" do - %w( create_index! delete_index! refresh_index! ).each do |method| - assert_respond_to subject, method - end - end - - context "mappings" do - should "configure the mappings for the type" do - subject.mappings do - indexes :title - end - - assert_equal( {:"my_document"=>{:properties=>{:title=>{:type=>"text"}}}}, subject.mappings.to_hash ) - end - end - - context "settings" do - should "configure the settings for the index" do - subject.settings foo: 'bar' - assert_equal( {foo: 'bar'}, subject.settings.to_hash) - end - end - - end -end diff --git a/elasticsearch-persistence/test/unit/repository_module_test.rb b/elasticsearch-persistence/test/unit/repository_module_test.rb deleted file mode 100644 index 2c4cd44c5..000000000 --- a/elasticsearch-persistence/test/unit/repository_module_test.rb +++ /dev/null @@ -1,146 +0,0 @@ -require 'test_helper' - -class Elasticsearch::Persistence::RepositoryModuleTest < Test::Unit::TestCase - context "The repository module" do - - class DummyModel - def initialize(attributes={}) - @attributes = attributes - end - - def to_hash - @attributes - end - - def inspect - "" - end - end - - setup do - class DummyRepository - include Elasticsearch::Persistence::Repository - end - end - - teardown do - Elasticsearch::Persistence::RepositoryModuleTest.__send__ :remove_const, :DummyRepository - end - - context "when included" do - should "set up the gateway for the class and instance" do - assert_respond_to DummyRepository, :gateway - assert_respond_to DummyRepository.new, :gateway - - assert_instance_of Elasticsearch::Persistence::Repository::Class, DummyRepository.gateway - assert_instance_of Elasticsearch::Persistence::Repository::Class, DummyRepository.new.gateway - end - - should "proxy repository methods from the class to the gateway" do - class DummyRepository - include Elasticsearch::Persistence::Repository - - index :foobar - klass DummyModel - type :my_dummy_model - mapping { indexes :title, analyzer: 'snowball' } - end - - repository = DummyRepository.new - - assert_equal :foobar, DummyRepository.index - assert_equal DummyModel, DummyRepository.klass - assert_equal :my_dummy_model, DummyRepository.type - assert_equal 'snowball', DummyRepository.mappings.to_hash[:my_dummy_model][:properties][:title][:analyzer] - - assert_equal :foobar, repository.index - assert_equal DummyModel, repository.klass - assert_equal :my_dummy_model, repository.type - assert_equal 'snowball', repository.mappings.to_hash[:my_dummy_model][:properties][:title][:analyzer] - end - - should "correctly delegate to the gateway" do - repository = DummyRepository.new - assert_instance_of Method, repository.method(:index) - end - - should "proxy repository methods from the instance to the gateway" do - class DummyRepository - include Elasticsearch::Persistence::Repository - end - - repository = DummyRepository.new - repository.index :foobar - repository.klass DummyModel - repository.type :my_dummy_model - repository.mapping { indexes :title, analyzer: 'snowball' } - - assert_equal :foobar, DummyRepository.index - assert_equal DummyModel, DummyRepository.klass - assert_equal :my_dummy_model, DummyRepository.type - assert_equal 'snowball', DummyRepository.mappings.to_hash[:my_dummy_model][:properties][:title][:analyzer] - - assert_equal :foobar, repository.index - assert_equal DummyModel, repository.klass - assert_equal :my_dummy_model, repository.type - assert_equal 'snowball', repository.mappings.to_hash[:my_dummy_model][:properties][:title][:analyzer] - end - - should "allow to define gateway methods in the class definition via block passed to the gateway method" do - class DummyRepositoryWithGatewaySerialize - include Elasticsearch::Persistence::Repository - - gateway do - def serialize(document) - 'FAKE!' - end - end - end - - repository = DummyRepositoryWithGatewaySerialize.new - repository.client.transport.logger = Logger.new(STDERR) - - client = mock - client.expects(:index).with do |arguments| - assert_equal('xxx', arguments[:id]) - assert_equal('FAKE!', arguments[:body]) - true - end - repository.gateway.expects(:client).returns(client) - - repository.gateway.expects(:__get_id_from_document).returns('xxx') - - repository.save( id: '123', foo: 'bar' ) - end - end - - should "allow to define gateway methods in the class definition via regular method definition" do - class DummyRepositoryWithDirectSerialize - include Elasticsearch::Persistence::Repository - - def serialize(document) - 'FAKE IN CLASS!' - end - end - - repository = DummyRepositoryWithDirectSerialize.new - repository.client.transport.logger = Logger.new(STDERR) - - client = mock - client.expects(:index).with do |arguments| - assert_equal('xxx', arguments[:id]) - assert_equal('FAKE IN CLASS!', arguments[:body]) - true - end - repository.gateway.expects(:client).returns(client) - - repository.gateway.expects(:__get_id_from_document).returns('xxx') - - repository.save( id: '123', foo: 'bar' ) - end - - should "configure the index name in the shortcut initializer" do - assert_equal 'repository', Elasticsearch::Persistence::Repository.new.index_name - end - end -end diff --git a/elasticsearch-persistence/test/unit/repository_naming_test.rb b/elasticsearch-persistence/test/unit/repository_naming_test.rb deleted file mode 100644 index aed34598f..000000000 --- a/elasticsearch-persistence/test/unit/repository_naming_test.rb +++ /dev/null @@ -1,117 +0,0 @@ -require 'test_helper' - -class Elasticsearch::Persistence::RepositoryNamingTest < Test::Unit::TestCase - context "The repository naming" do - # Fake class for the naming tests - class ::Foobar; end - class ::FooBar; end - module ::Foo; class Bar; end; end - - setup do - @shoulda_subject = Class.new() { include Elasticsearch::Persistence::Repository::Naming }.new - end - - context "get an ID from the document" do - should "get an ID from Hash" do - assert_equal 1, subject.__get_id_from_document(id: 1) - assert_equal 1, subject.__get_id_from_document(_id: 1) - assert_equal 1, subject.__get_id_from_document('id' => 1) - assert_equal 1, subject.__get_id_from_document('_id' => 1) - end - end - - context "extract an ID from the document" do - should "delete the key from the Hash" do - d1 = { :id => 1 } - d2 = { :_id => 1 } - d3 = { 'id' => 1 } - d4 = { '_id' => 1 } - - assert_equal 1, subject.__extract_id_from_document(d1) - assert_nil d1[:id] - - assert_equal 1, subject.__extract_id_from_document(d2) - assert_nil d1[:_id] - - assert_equal 1, subject.__extract_id_from_document(d3) - assert_nil d1['id'] - - assert_equal 1, subject.__extract_id_from_document(d4) - assert_nil d1['_id'] - end - end - - context "document class name" do - should "be nil by default" do - assert_nil subject.klass - end - - should "be settable" do - subject.klass = Foobar - assert_equal Foobar, subject.klass - end - - should "be settable by DSL" do - subject.klass Foobar - assert_equal Foobar, subject.klass - end - end - - context "index_name" do - should "default to the class name" do - subject.instance_eval do - def self.class - 'FakeRepository' - end - end - - assert_equal 'fake_repository', subject.index_name - end - - should "be settable" do - subject.index_name = 'foobar1' - assert_equal 'foobar1', subject.index_name - - subject.index_name 'foobar2' - assert_equal 'foobar2', subject.index_name - end - - should "be aliased as `index`" do - subject.index_name = 'foobar1' - assert_equal 'foobar1', subject.index - end - - should "be inferred from the host class" do - class ::MySpecialRepository; end - subject.define_singleton_method(:host) { MySpecialRepository } - assert_equal 'my_special_repository', subject.index_name - end - end - - context "document_type" do - should "be the default doc type when no klass is set" do - assert_equal '_doc', subject.document_type - end - - should "does not use the klass" do - subject.klass Foobar - assert_equal '_doc', subject.document_type - end - - should "be aliased as `type`" do - subject.document_type 'foobar' - assert_equal 'foobar', subject.type - end - - should "be settable" do - subject.document_type = 'foobar' - assert_equal 'foobar', subject.document_type - end - - should "be settable by DSL" do - subject.document_type 'foobar' - assert_equal 'foobar', subject.document_type - end - end - end -end diff --git a/elasticsearch-persistence/test/unit/repository_response_results_test.rb b/elasticsearch-persistence/test/unit/repository_response_results_test.rb deleted file mode 100644 index df8e08ae5..000000000 --- a/elasticsearch-persistence/test/unit/repository_response_results_test.rb +++ /dev/null @@ -1,98 +0,0 @@ -require 'test_helper' - -class Elasticsearch::Persistence::RepositoryResponseResultsTest < Test::Unit::TestCase - include Elasticsearch::Persistence - class MyDocument; end - - context "Response results" do - setup do - @repository = Repository.new - - @response = { "took" => 2, - "timed_out" => false, - "_shards" => {"total" => 5, "successful" => 5, "failed" => 0}, - "hits" => - { "total" => 2, - "max_score" => 0.19, - "hits" => - [{"_index" => "my_index", - "_type" => "note", - "_id" => "1", - "_score" => 0.19, - "_source" => {"id" => 1, "title" => "Test 1"}}, - - {"_index" => "my_index", - "_type" => "note", - "_id" => "2", - "_score" => 0.19, - "_source" => {"id" => 2, "title" => "Test 2"}} - ] - } - } - - @shoulda_subject = Repository::Response::Results.new @repository, @response - end - - should "provide the access to the repository" do - assert_instance_of Repository::Class, subject.repository - end - - should "provide the access to the response" do - assert_equal 5, subject.response['_shards']['total'] - end - - should "wrap the response in HashWrapper" do - assert_equal 5, subject.response._shards.total - end - - should "return the total" do - assert_equal 2, subject.total - end - - should "return the max_score" do - assert_equal 0.19, subject.max_score - end - - should "delegate methods to results" do - subject.repository - .expects(:deserialize) - .twice - .returns(MyDocument.new) - - assert_equal 2, subject.size - assert_respond_to subject, :each - end - - should "respond to missing" do - assert_instance_of Method, subject.method(:to_a) - end - - should "yield each object with hit" do - @shoulda_subject = Repository::Response::Results.new \ - @repository, - { 'hits' => { 'hits' => [{'_id' => '1', 'foo' => 'bar'}] } } - - subject.repository - .expects(:deserialize) - .returns('FOO') - - subject.each_with_hit do |object, hit| - assert_equal 'FOO', object - assert_equal 'bar', hit.foo - end - end - - should "map objects and hits" do - @shoulda_subject = Repository::Response::Results.new \ - @repository, - { 'hits' => { 'hits' => [{'_id' => '1', 'foo' => 'bar'}] } } - - subject.repository - .expects(:deserialize) - .returns('FOO') - - assert_equal ['FOO---bar'], subject.map_with_hit { |object, hit| "#{object}---#{hit.foo}" } - end - end - -end diff --git a/elasticsearch-persistence/test/unit/repository_search_test.rb b/elasticsearch-persistence/test/unit/repository_search_test.rb deleted file mode 100644 index 5cf6d5ab3..000000000 --- a/elasticsearch-persistence/test/unit/repository_search_test.rb +++ /dev/null @@ -1,100 +0,0 @@ -require 'test_helper' - -class Elasticsearch::Persistence::RepositorySearchTest < Test::Unit::TestCase - class MyDocument; end - - context "The repository search" do - setup do - @shoulda_subject = Class.new() { include Elasticsearch::Persistence::Repository::Search }.new - - @client = mock - @shoulda_subject.stubs(:document_type).returns(nil) - @shoulda_subject.stubs(:klass).returns(nil) - @shoulda_subject.stubs(:index_name).returns('test') - @shoulda_subject.stubs(:client).returns(@client) - end - - should "search in type does not use the klass setting" do - @client.expects(:search).with do |arguments| - assert_equal 'test', arguments[:index] - assert_equal({foo: 'bar'}, arguments[:body]) - true - end - - subject.search foo: 'bar' - end - - should "search in type based on document_type" do - subject.expects(:document_type).returns('my_special_document').at_least_once - - @client.expects(:search).with do |arguments| - assert_equal 'test', arguments[:index] - assert_equal 'my_special_document', arguments[:type] - assert_equal({foo: 'bar'}, arguments[:body]) - true - end - - subject.search foo: 'bar' - end - - should "search across all types" do - subject.expects(:document_type).returns(nil).at_least_once - - @client.expects(:search).with do |arguments| - assert_equal 'test', arguments[:index] - assert_equal '_all', arguments[:type] - assert_equal({foo: 'bar'}, arguments[:body]) - true - end - - assert_instance_of Elasticsearch::Persistence::Repository::Response::Results, - subject.search({ foo: 'bar' }, type: '_all') - end - - should "pass options to the client" do - @client.expects(:search).twice.with do |arguments| - assert_equal 'bambam', arguments[:routing] - true - end - - assert_instance_of Elasticsearch::Persistence::Repository::Response::Results, - subject.search( {foo: 'bar'}, { routing: 'bambam' } ) - assert_instance_of Elasticsearch::Persistence::Repository::Response::Results, - subject.search( 'foobar', { routing: 'bambam' } ) - end - - should "search with simple search" do - @client.expects(:search).with do |arguments| - assert_equal 'foobar', arguments[:q] - true - end - - assert_instance_of Elasticsearch::Persistence::Repository::Response::Results, - subject.search('foobar') - end - - should "raise error for incorrect search definitions" do - assert_raise ArgumentError do - subject.search 123 - end - end - - should "return the number of domain objects" do - subject.client.expects(:count).returns({'count' => 1}) - assert_equal 1, subject.count - end - - should "pass arguments to count" do - subject.client.expects(:count) - .with do |arguments| - assert_equal 'test', arguments[:index] - assert_equal 'bar', arguments[:body][:query][:match][:foo] - assert_equal true, arguments[:ignore_unavailable] - true - end - .returns({'count' => 1}) - - assert_equal 1, subject.count( { query: { match: { foo: 'bar' } } }, { ignore_unavailable: true } ) - end - end -end diff --git a/elasticsearch-persistence/test/unit/repository_serialize_test.rb b/elasticsearch-persistence/test/unit/repository_serialize_test.rb deleted file mode 100644 index a69145464..000000000 --- a/elasticsearch-persistence/test/unit/repository_serialize_test.rb +++ /dev/null @@ -1,41 +0,0 @@ -require 'test_helper' - -class Elasticsearch::Persistence::RepositorySerializeTest < Test::Unit::TestCase - context "The repository serialization" do - class DummyDocument - def to_hash - { foo: 'bar' } - end - end - - class MyDocument; end - - setup do - @shoulda_subject = Class.new() { include Elasticsearch::Persistence::Repository::Serialize }.new - end - - context "serialize" do - should "call #to_hash on passed object" do - document = DummyDocument.new - assert_equal( { foo: 'bar' }, subject.serialize(document)) - end - end - - context "deserialize" do - should "get the class name from #klass" do - subject.expects(:klass) - .returns(MyDocument).twice - - MyDocument.expects(:new) - - subject.deserialize( {} ) - end - - should "raise an error when klass isn't set" do - subject.expects(:klass).returns(nil) - - assert_raise(NameError) { subject.deserialize( {} ) } - end - end - end -end diff --git a/elasticsearch-persistence/test/unit/repository_store_test.rb b/elasticsearch-persistence/test/unit/repository_store_test.rb deleted file mode 100644 index b886eecdf..000000000 --- a/elasticsearch-persistence/test/unit/repository_store_test.rb +++ /dev/null @@ -1,219 +0,0 @@ -require 'test_helper' - -class Elasticsearch::Persistence::RepositoryStoreTest < Test::Unit::TestCase - context "The repository store" do - class MyDocument; end - - setup do - @shoulda_subject = Class.new() do - include Elasticsearch::Persistence::Repository::Store - include Elasticsearch::Persistence::Repository::Naming - end.new - @shoulda_subject.stubs(:index_name).returns('test') - end - - context "save" do - - should "serialize the document, get type from document_type and index it" do - subject.expects(:serialize).returns({foo: 'bar'}) - - subject.expects(:document_type).returns('my_document') - - subject.expects(:__get_id_from_document).returns('1') - - client = mock - client.expects(:index).with do |arguments| - assert_equal 'my_document', arguments[:type] - assert_equal '1', arguments[:id] - assert_equal({foo: 'bar'}, arguments[:body]) - true - end - subject.expects(:client).returns(client) - - subject.save(MyDocument.new) - end - - should "pass the options to the client" do - subject.expects(:serialize).returns({foo: 'bar'}) - subject.expects(:document_type).returns(nil) - subject.expects(:__get_id_from_document).returns('1') - - client = mock - client.expects(:index).with do |arguments| - assert_equal 'my_document', arguments[:type] - assert_equal 'foobarbam', arguments[:index] - assert_equal 'bambam', arguments[:routing] - true - end - subject.expects(:client).returns(client) - - subject.save({foo: 'bar'}, { index: 'foobarbam', routing: 'bambam', type: 'my_document' }) - end - end - - context "update" do - should "get the ID from first argument and :doc from options" do - subject.expects(:serialize).never - subject.expects(:document_type).returns('mydoc') - subject.expects(:__extract_id_from_document).never - - client = mock - client.expects(:update).with do |arguments| - assert_equal '1', arguments[:id] - assert_equal 'mydoc', arguments[:type] - assert_equal({doc: { foo: 'bar' }}, arguments[:body]) - true - end - subject.expects(:client).returns(client) - - subject.update('1', doc: { foo: 'bar' }) - end - - should "get the ID from first argument and :script from options" do - subject.expects(:document_type).returns('mydoc') - subject.expects(:__extract_id_from_document).never - - client = mock - client.expects(:update).with do |arguments| - assert_equal '1', arguments[:id] - assert_equal 'mydoc', arguments[:type] - assert_equal({script: 'ctx._source.foo += 1'}, arguments[:body]) - true - end - subject.expects(:client).returns(client) - - subject.update('1', script: 'ctx._source.foo += 1') - end - - should "get the ID from first argument and :script with :upsert from options" do - subject.expects(:document_type).returns('mydoc') - subject.expects(:__extract_id_from_document).never - - client = mock - client.expects(:update).with do |arguments| - assert_equal '1', arguments[:id] - assert_equal 'mydoc', arguments[:type] - assert_equal({script: 'ctx._source.foo += 1', upsert: { foo: 1 }}, arguments[:body]) - true - end - subject.expects(:client).returns(client) - - subject.update('1', script: 'ctx._source.foo += 1', upsert: { foo: 1 }) - end - - should "get the ID and :doc from document" do - subject.expects(:document_type).returns('mydoc') - - client = mock - client.expects(:update).with do |arguments| - assert_equal '1', arguments[:id] - assert_equal 'mydoc', arguments[:type] - assert_equal({doc: { foo: 'bar' }}, arguments[:body]) - true - end - subject.expects(:client).returns(client) - - subject.update(id: '1', foo: 'bar') - end - - should "get the ID and :script from document" do - subject.expects(:document_type).returns('mydoc') - - client = mock - client.expects(:update).with do |arguments| - assert_equal '1', arguments[:id] - assert_equal 'mydoc', arguments[:type] - assert_equal({script: 'ctx._source.foo += 1'}, arguments[:body]) - true - end - subject.expects(:client).returns(client) - - subject.update(id: '1', script: 'ctx._source.foo += 1') - end - - should "get the ID and :script with :upsert from document" do - subject.expects(:document_type).returns('mydoc') - - client = mock - client.expects(:update).with do |arguments| - assert_equal '1', arguments[:id] - assert_equal 'mydoc', arguments[:type] - assert_equal({script: 'ctx._source.foo += 1', upsert: { foo: 1 } }, arguments[:body]) - true - end - subject.expects(:client).returns(client) - - subject.update(id: '1', script: 'ctx._source.foo += 1', upsert: { foo: 1 }) - end - - should "override the type from params" do - subject.expects(:document_type).never - - client = mock - client.expects(:update).with do |arguments| - assert_equal '1', arguments[:id] - assert_equal 'foo', arguments[:type] - assert_equal({script: 'ctx._source.foo += 1'}, arguments[:body]) - true - end - subject.expects(:client).returns(client) - - subject.update(id: '1', script: 'ctx._source.foo += 1', type: 'foo') - end - - should "raise an exception when passed incorrect argument" do - assert_raise(ArgumentError) { subject.update(MyDocument.new, foo: 'bar') } - end - end - - context "delete" do - - should "get ID from document and type from document_type when passed a document" do - subject.expects(:serialize).returns({id: '1', foo: 'bar'}) - - subject.expects(:document_type).returns('my_document') - - subject.expects(:__get_id_from_document).with({id: '1', foo: 'bar'}).returns('1') - - client = mock - client.expects(:delete).with do |arguments| - assert_equal 'my_document', arguments[:type] - assert_equal '1', arguments[:id] - true - end - subject.expects(:client).returns(client) - - subject.delete({id: '1', foo: 'bar'}) - end - - should "get ID and uses the default document type" do - subject.expects(:serialize).returns({id: '1', foo: 'bar'}) - subject.expects(:document_type).returns('_doc') - subject.expects(:__get_id_from_document).with({id: '1', foo: 'bar'}).returns('1') - - client = mock - client.expects(:delete).with do |arguments| - assert_equal '_doc', arguments[:type] - assert_equal '1', arguments[:id] - true - end - subject.expects(:client).returns(client) - - subject.delete(MyDocument.new) - end - - should "pass the options to the client" do - client = mock - client.expects(:delete).with do |arguments| - assert_equal 'my_document', arguments[:type] - assert_equal 'foobarbam', arguments[:index] - assert_equal 'bambam', arguments[:routing] - true - end - subject.expects(:client).returns(client) - - subject.delete('1', index: 'foobarbam', routing: 'bambam', type: 'my_document') - end - end - end -end