From 1bdd22d9d0e0d6a67d1e09c7c19b0e9bda85ee7e Mon Sep 17 00:00:00 2001 From: Emily Stolfo Date: Thu, 5 Jul 2018 13:22:04 +0200 Subject: [PATCH] [STORE] Remove Elasticsearch::Persistence::Model (ActiveRecord persistence pattern) --- README.md | 14 - elasticsearch-persistence/README.md | 259 +------- .../lib/elasticsearch/persistence/model.rb | 135 ---- .../elasticsearch/persistence/model/base.rb | 87 --- .../elasticsearch/persistence/model/errors.rb | 8 - .../elasticsearch/persistence/model/find.rb | 180 ------ .../elasticsearch/persistence/model/rails.rb | 47 -- .../elasticsearch/persistence/model/store.rb | 254 -------- .../elasticsearch/persistence/model/utils.rb | 0 .../elasticsearch/model/model_generator.rb | 21 - .../elasticsearch/model/templates/model.rb.tt | 9 - .../generators/elasticsearch_generator.rb | 2 - .../integration/model/model_basic_test.rb | 233 ------- .../test/unit/model_base_test.rb | 72 --- .../test/unit/model_find_test.rb | 153 ----- .../test/unit/model_gateway_test.rb | 101 --- .../test/unit/model_rails_test.rb | 112 ---- .../test/unit/model_store_test.rb | 576 ------------------ .../rails/instrumentation/railtie.rb | 4 - .../lib/elasticsearch/rails/lograge.rb | 4 - 20 files changed, 2 insertions(+), 2269 deletions(-) delete mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/model.rb delete mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/model/base.rb delete mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/model/errors.rb delete mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/model/find.rb delete mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/model/rails.rb delete mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/model/store.rb delete mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/model/utils.rb delete mode 100644 elasticsearch-persistence/lib/rails/generators/elasticsearch/model/model_generator.rb delete mode 100644 elasticsearch-persistence/lib/rails/generators/elasticsearch/model/templates/model.rb.tt delete mode 100644 elasticsearch-persistence/lib/rails/generators/elasticsearch_generator.rb delete mode 100644 elasticsearch-persistence/test/integration/model/model_basic_test.rb delete mode 100644 elasticsearch-persistence/test/unit/model_base_test.rb delete mode 100644 elasticsearch-persistence/test/unit/model_find_test.rb delete mode 100644 elasticsearch-persistence/test/unit/model_gateway_test.rb delete mode 100644 elasticsearch-persistence/test/unit/model_rails_test.rb delete mode 100644 elasticsearch-persistence/test/unit/model_store_test.rb diff --git a/README.md b/README.md index f7c8e1151..ef1a8476e 100644 --- a/README.md +++ b/README.md @@ -98,20 +98,6 @@ repository.save Article.new(title: 'Test') # => {"_index"=>"repository", "_type"=>"article", "_id"=>"Ak75E0U9Q96T5Y999_39NA", ...} ``` -Example of using Elasticsearch as a persistence layer for a Ruby model: - -```ruby -require 'elasticsearch/persistence/model' -class Article - include Elasticsearch::Persistence::Model - attribute :title, String, mapping: { analyzer: 'snowball' } -end - -Article.create title: 'Test' -# POST http://localhost:9200/articles/article -# => #
-``` - **Please refer to each library documentation for detailed information and examples.** ### Model diff --git a/elasticsearch-persistence/README.md b/elasticsearch-persistence/README.md index 4c6dead10..e95a7f005 100644 --- a/elasticsearch-persistence/README.md +++ b/elasticsearch-persistence/README.md @@ -38,7 +38,6 @@ or install it from a source code checkout: The library provides two different patterns for adding persistence to your Ruby objects: * [Repository Pattern](#the-repository-pattern) -* [ActiveRecord Pattern](#the-activerecord-pattern) ### The Repository Pattern @@ -445,262 +444,8 @@ and demonstrates a rich set of features: ### The ActiveRecord Pattern -The `Elasticsearch::Persistence::Model` module provides an implementation of the -active record [pattern](http://www.martinfowler.com/eaaCatalog/activeRecord.html), -with a familiar interface for using Elasticsearch as a persistence layer in -Ruby on Rails applications. - -All the methods are documented with comprehensive examples in the source code, -available also online at . - -#### Installation/Usage - -To use the library in a Rails application, add it to your `Gemfile` with a `require` statement: - -```ruby -gem "elasticsearch-persistence", require: 'elasticsearch/persistence/model' -``` - -To use the library without Bundler, install it, and require the file: - -```bash -gem install elasticsearch-persistence -``` - -```ruby -# In your code -require 'elasticsearch/persistence/model' -``` - -#### Model Definition - -The integration is implemented by including the module in a Ruby class. -The model attribute definition support is implemented with the -[_Virtus_](https://github.com/solnic/virtus) Rubygem, and the -naming, validation, etc. features with the -[_ActiveModel_](https://github.com/rails/rails/tree/master/activemodel) Rubygem. - -```ruby -class Article - include Elasticsearch::Persistence::Model - - # Define a plain `title` attribute - # - attribute :title, String - - # Define an `author` attribute, with multiple analyzers for this field - # - attribute :author, String, mapping: { fields: { - author: { type: 'text'}, - raw: { type: 'keyword' } - } } - - - # Define a `views` attribute, with default value - # - attribute :views, Integer, default: 0, mapping: { type: 'integer' } - - # Validate the presence of the `title` attribute - # - validates :title, presence: true - - # Execute code after saving the model. - # - after_save { puts "Successfully saved: #{self}" } -end -``` - -Attribute validations work like for any other _ActiveModel_-compatible implementation: - -```ruby -article = Article.new # => #
- -article.valid? -# => false - -article.errors.to_a -# => ["Title can't be blank"] -``` - -#### Persistence - -We can create a new article in the database... - -```ruby -Article.create id: 1, title: 'Test', author: 'John' -# PUT http://localhost:9200/articles/article/1 [status:201, request:0.015s, query:n/a] -``` - -... and find it: - -```ruby -article = Article.find(1) -# => #
- -article._index -# => "articles" - -article.id -# => "1" - -article.title -# => "Test" -``` - -To update the model, either update the attribute and save the model: - -```ruby -article.title = 'Updated' - -article.save -# => {"_index"=>"articles", "_type"=>"article", "_id"=>"1", "_version"=>2, "created"=>false} -``` - -... or use the `update_attributes` method: - -```ruby -article.update_attributes title: 'Test', author: 'Mary' -# => {"_index"=>"articles", "_type"=>"article", "_id"=>"1", "_version"=>3} -``` - -The implementation supports the familiar interface for updating model timestamps: - -```ruby -article.touch -# => => { ... "_version"=>4} -``` - -... and numeric attributes: - -```ruby -article.views -# => 0 - -article.increment :views -article.views -# => 1 -``` - -Any callbacks defined in the model will be triggered during the persistence operations: - -```ruby -article.save -# Successfully saved: #
-``` - -The model also supports familiar `find_in_batches` and `find_each` methods to efficiently -retrieve big collections of model instances, using the Elasticsearch's _Scan API_: - -```ruby -Article.find_each(_source_include: 'title') { |a| puts "===> #{a.title.upcase}" } -# GET http://localhost:9200/articles/article/_search?scroll=5m&size=20 -# GET http://localhost:9200/_search/scroll?scroll=5m&scroll_id=c2Nhb... -# ===> TEST -# GET http://localhost:9200/_search/scroll?scroll=5m&scroll_id=c2Nhb... -# => "c2Nhb..." -``` - -#### Search - -The model class provides a `search` method to retrieve model instances with a regular -search definition, including highlighting, aggregations, etc: - -```ruby -results = Article.search query: { match: { title: 'test' } }, - aggregations: { authors: { terms: { field: 'author.raw' } } }, - highlight: { fields: { title: {} } } - -puts results.first.title -# Test - -puts results.first.hit.highlight['title'] -# Test - -puts results.response.aggregations.authors.buckets.each { |b| puts "#{b['key']} : #{b['doc_count']}" } -# John : 1 -``` - -#### The Elasticsearch Client - -The module will set up a [client](https://github.com/elastic/elasticsearch-ruby/tree/master/elasticsearch), -connected to `localhost:9200`, by default. - -To use a client with different configuration: - -```ruby -Elasticsearch::Persistence.client = Elasticsearch::Client.new log: true -``` - -To set up a specific client for a specific model: - -```ruby -Article.gateway.client = Elasticsearch::Client.new host: 'api.server.org' -``` - -You might want to do this during you application bootstrap process, e.g. in a Rails initializer. - -Please refer to the -[`elasticsearch-transport`](https://github.com/elasticsearch/elasticsearch-ruby/tree/master/elasticsearch-transport) -library documentation for all the configuration options, and to the -[`elasticsearch-api`](http://rubydoc.info/gems/elasticsearch-api) library documentation -for information about the Ruby client API. - -#### Accessing the Repository Gateway and the Client - -The integration with Elasticsearch is implemented by embedding the repository object in the model. -You can access it through the `gateway` method: - -```ruby -Artist.gateway.client.info -# GET http://localhost:9200/ [status:200, request:0.011s, query:n/a] -# => {"status"=>200, "name"=>"Lightspeed", ...} -``` - -#### Rails Compatibility - -The model instances are fully compatible with Rails' conventions and helpers: - -```ruby -url_for article -# => "http://localhost:3000/articles/1" - -div_for article -# => '
' -``` - -... as well as form values for dates and times: - -```ruby -article = Article.new "title" => "Date", "published(1i)"=>"2014", "published(2i)"=>"1", "published(3i)"=>"1" - -article.published.iso8601 -# => "2014-01-01" -``` - -The library provides a Rails ORM generator to facilitate building the application scaffolding: - -```bash -rails generate scaffold Person name:String email:String birthday:Date --orm=elasticsearch -``` - -#### Example application - -A fully working Ruby on Rails application can be generated with the following command: - -```bash -rails new music --force --skip --skip-bundle --skip-active-record --template https://raw.githubusercontent.com/elastic/elasticsearch-rails/master/elasticsearch-persistence/examples/music/template.rb -``` - -The application demonstrates: - -* How to set up model attributes with custom mappings -* How to define model relationships with Elasticsearch's parent/child -* How to configure models to use a common index, and create the index with proper mappings -* How to use Elasticsearch's completion suggester to drive auto-complete functionality -* How to use Elasticsearch-persisted models in Rails' views and forms -* How to write controller tests - -The source files for the application are available in the [`examples/music`](examples/music) folder. +The ActiveRecord pattern has been deprecated as of version 6.0.0 of this gem. Please use the +[Repository Pattern](#the-repository-pattern) instead. ## License diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/model.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/model.rb deleted file mode 100644 index f6cd50eb0..000000000 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/model.rb +++ /dev/null @@ -1,135 +0,0 @@ -require 'active_support/core_ext/module/delegation' - -require 'active_model' -require 'virtus' - -require 'elasticsearch/persistence' -require 'elasticsearch/persistence/model/base' -require 'elasticsearch/persistence/model/errors' -require 'elasticsearch/persistence/model/store' -require 'elasticsearch/persistence/model/find' - -module Elasticsearch - module Persistence - - # When included, extends a plain Ruby class with persistence-related features via the ActiveRecord pattern - # - # @example Include the repository in a custom class - # - # require 'elasticsearch/persistence/model' - # - # class MyObject - # include Elasticsearch::Persistence::Model - # end - # - module Model - def self.included(base) - base.class_eval do - include ActiveModel::Naming - include ActiveModel::Conversion - include ActiveModel::Serialization - include ActiveModel::Serializers::JSON - include ActiveModel::Validations - - include Virtus.model - - extend ActiveModel::Callbacks - define_model_callbacks :create, :save, :update, :destroy - define_model_callbacks :find, :touch, only: :after - - include Elasticsearch::Persistence::Model::Base::InstanceMethods - - extend Elasticsearch::Persistence::Model::Store::ClassMethods - include Elasticsearch::Persistence::Model::Store::InstanceMethods - - extend Elasticsearch::Persistence::Model::Find::ClassMethods - - class << self - # Re-define the Virtus' `attribute` method, to configure Elasticsearch mapping as well - # - def attribute(name, type=nil, options={}, &block) - mapping = options.delete(:mapping) || {} - super - - gateway.mapping do - indexes name, {type: Utils::lookup_type(type)}.merge(mapping) - end - - gateway.mapping(&block) if block_given? - end - - # Return the {Repository::Class} instance - # - def gateway(&block) - @gateway ||= Elasticsearch::Persistence::Repository::Class.new host: self - block.arity < 1 ? @gateway.instance_eval(&block) : block.call(@gateway) if block_given? - @gateway - end - - # Delegate methods to repository - # - delegate :settings, - :mappings, - :mapping, - :document_type=, - :index_name, - :index_name=, - :find, - :exists?, - :create_index!, - :refresh_index!, - to: :gateway - - # forward document type to mappings when set - def document_type(type = nil) - return gateway.document_type unless type - gateway.document_type type - mapping.type = type - end - end - - # Configure the repository based on the model (set up index_name, etc) - # - gateway do - klass base - index_name base.model_name.collection.gsub(/\//, '-') - document_type base.model_name.element - - def serialize(document) - document.to_hash.except(:id, 'id') - end - - def deserialize(document) - object = klass.new document['_source'] - - # Set the meta attributes when fetching the document from Elasticsearch - # - object.instance_variable_set :@_id, document['_id'] - object.instance_variable_set :@_index, document['_index'] - object.instance_variable_set :@_type, document['_type'] - object.instance_variable_set :@_version, document['_version'] - object.instance_variable_set :@_source, document['_source'] - - # Store the "hit" information (highlighting, score, ...) - # - object.instance_variable_set :@hit, - Elasticsearch::Model::HashWrapper.new(document.except('_index', '_type', '_id', '_version', '_source')) - - object.instance_variable_set(:@persisted, true) - object - end - end - - # Set up common attributes - # - attribute :created_at, Time, default: lambda { |o,a| Time.now.utc } - attribute :updated_at, Time, default: lambda { |o,a| Time.now.utc } - - attr_reader :hit - end - - end - end - - end -end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/model/base.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/model/base.rb deleted file mode 100644 index a0af32411..000000000 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/model/base.rb +++ /dev/null @@ -1,87 +0,0 @@ -module Elasticsearch - module Persistence - module Model - # This module contains the base interface for models - # - module Base - module InstanceMethods - - # Model initializer sets the `@id` variable if passed - # - def initialize(attributes={}) - @_id = attributes[:id] || attributes['id'] - super - end - - # Return model attributes as a Hash, merging in the `id` - # - def attributes - super.merge id: id - end - - # Return the document `_id` - # - def id - @_id - end; alias :_id :id - - # Set the document `_id` - # - def id=(value) - @_id = value - end; alias :_id= :id= - - # Return the document `_index` - # - def _index - @_index - end - - # Return the document `_type` - # - def _type - @_type - end - - # Return the document `_version` - # - def _version - @_version - end - - # Return the raw document `_source` - # - def _source - @_source - end - - def to_s - "#<#{self.class} #{attributes.to_hash.inspect.gsub(/:(\w+)=>/, '\1: ')}>" - end; alias :inspect :to_s - end - end - - # Utility methods for {Elasticsearch::Persistence::Model} - # - module Utils - - # Return Elasticsearch type based on passed Ruby class (used in the `attribute` method) - # - def lookup_type(type) - case - when type == String - 'text' - when type == Integer - 'integer' - when type == Float - 'float' - when type == Date || type == Time || type == DateTime - 'date' - when type == Virtus::Attribute::Boolean - 'boolean' - end - end; module_function :lookup_type - end - end - end -end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/model/errors.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/model/errors.rb deleted file mode 100644 index 3d8ebb88e..000000000 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/model/errors.rb +++ /dev/null @@ -1,8 +0,0 @@ -module Elasticsearch - module Persistence - module Model - class DocumentNotSaved < StandardError; end - class DocumentNotPersisted < StandardError; end - end - end -end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/model/find.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/model/find.rb deleted file mode 100644 index c09c9ac38..000000000 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/model/find.rb +++ /dev/null @@ -1,180 +0,0 @@ -module Elasticsearch - module Persistence - module Model - - module Find - class SearchRequest < Elasticsearch::Model::Searching::SearchRequest - def execute! - klass.gateway.search(definition[:body] || definition[:q], options) - end - end - - module ClassMethods - - def search(query_or_definition, options={}) - SearchRequest.new(self, query_or_definition, options).execute! - end - - # Returns all models (up to 10,000) - # - # @example Retrieve all people - # - # Person.all - # # => [# [# 2 - # - # @example Return the count of models matching a simple query - # - # Person.count('fox or dog') - # # => 1 - # - # @example Return the count of models matching a query in the Elasticsearch DSL - # - # Person.search(query: { match: { title: 'fox dog' } }) - # # => 1 - # - # @return [Integer] - # - def count(query_or_definition=nil, options={}) - gateway.count( query_or_definition, options ) - end - - # Returns all models efficiently via the Elasticsearch's scroll API - # - # You can restrict the models being returned with a query. - # - # The {http://rubydoc.info/gems/elasticsearch-api/Elasticsearch/API/Actions#search-instance_method Search API} - # options are passed to the search method as parameters, all remaining options are passed - # as the `:body` parameter. - # - # The full {Persistence::Repository::Response::Results} instance is yielded to the passed - # block in each batch, so you can access any of its properties; calling `to_a` will - # convert the object to an Array of model instances. - # - # @example Return all models in batches of 20 x number of primary shards - # - # Person.find_in_batches { |batch| puts batch.map(&:name) } - # - # @example Return all models in batches of 100 x number of primary shards - # - # Person.find_in_batches(size: 100) { |batch| puts batch.map(&:name) } - # - # @example Return all models matching a specific query - # - # Person.find_in_batches(query: { match: { name: 'test' } }) { |batch| puts batch.map(&:name) } - # - # @example Return all models, fetching only the `name` attribute from Elasticsearch - # - # Person.find_in_batches( _source_include: 'name') { |_| puts _.response.hits.hits.map(&:to_hash) } - # - # @example Leave out the block to return an Enumerator instance - # - # Person.find_in_batches(size: 100).map { |batch| batch.size } - # # => [100, 100, 100, ... ] - # - # @return [String,Enumerator] The `scroll_id` for the request or Enumerator when the block is not passed - # - def find_in_batches(options={}, &block) - return to_enum(:find_in_batches, options) unless block_given? - - search_params = options.extract!( - :index, - :type, - :scroll, - :size, - :explain, - :ignore_indices, - :ignore_unavailable, - :allow_no_indices, - :expand_wildcards, - :preference, - :q, - :routing, - :source, - :_source, - :_source_include, - :_source_exclude, - :stats, - :timeout) - - scroll = search_params.delete(:scroll) || '5m' - - body = options - - # Get the initial batch of documents and the scroll_id - # - response = gateway.client.search( { - index: gateway.index_name, - type: gateway.document_type, - scroll: scroll, - sort: ['_doc'], - size: 20, - body: body }.merge(search_params) ) - - - # Scroll the search object and break when receiving an empty array of hits - # - while response['hits']['hits'].any? do - yield Repository::Response::Results.new(gateway, response) - - response = gateway.client.scroll( { scroll_id: response['_scroll_id'], scroll: scroll } ) - end - - return response['_scroll_id'] - end - - # Iterate effectively over models using the `find_in_batches` method. - # - # All the options are passed to `find_in_batches` and each result is yielded to the passed block. - # - # @example Print out the people's names by scrolling through the index - # - # Person.find_each { |person| puts person.name } - # - # # # GET http://localhost:9200/people/person/_search?scroll=5m&size=20 - # # # GET http://localhost:9200/_search/scroll?scroll=5m&scroll_id=c2Nhbj... - # # Test 0 - # # Test 1 - # # Test 2 - # # ... - # # # GET http://localhost:9200/_search/scroll?scroll=5m&scroll_id=c2Nhbj... - # # Test 20 - # # Test 21 - # # Test 22 - # - # @example Leave out the block to return an Enumerator instance - # - # Person.find_each.select { |person| person.name =~ /John/ } - # # => => [#] - # - # @return [String,Enumerator] The `scroll_id` for the request or Enumerator when the block is not passed - # - def find_each(options = {}) - return to_enum(:find_each, options) unless block_given? - - find_in_batches(options) do |batch| - batch.each { |result| yield result } - end - end - end - end - - end - end -end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/model/rails.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/model/rails.rb deleted file mode 100644 index 5ed510e21..000000000 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/model/rails.rb +++ /dev/null @@ -1,47 +0,0 @@ -module Elasticsearch - module Persistence - module Model - - # Make the `Persistence::Model` models compatible with Ruby On Rails applications - # - module Rails - def self.included(base) - base.class_eval do - - def initialize(attributes={}) - super(__convert_rails_dates(attributes)) - end - - def update(attributes={}, options={}) - super(__convert_rails_dates(attributes), options) - end - end - end - - # Decorates the passed in `attributes` so they extract the date & time values from Rails forms - # - # @example Correctly combine the date and time to a datetime string - # - # params = { "published_on(1i)"=>"2014", - # "published_on(2i)"=>"1", - # "published_on(3i)"=>"1", - # "published_on(4i)"=>"12", - # "published_on(5i)"=>"00" - # } - # MyRailsModel.new(params).published_on.iso8601 - # # => "2014-01-01T12:00:00+00:00" - # - def __convert_rails_dates(attributes={}) - day = attributes.select { |p| p =~ /\([1-3]/ }.reduce({}) { |sum, item| (sum[item.first.gsub(/\(.+\)/, '')] ||= '' )<< item.last+'-'; sum } - time = attributes.select { |p| p =~ /\([4-6]/ }.reduce({}) { |sum, item| (sum[item.first.gsub(/\(.+\)/, '')] ||= '' )<< item.last+':'; sum } - unless day.empty? - attributes.update day.reduce({}) { |sum, item| sum[item.first] = item.last; sum[item.first] += ' ' + time[item.first] unless time.empty?; sum } - end - - return attributes - end; module_function :__convert_rails_dates - end - - end - end -end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/model/store.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/model/store.rb deleted file mode 100644 index a6c44c998..000000000 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/model/store.rb +++ /dev/null @@ -1,254 +0,0 @@ -module Elasticsearch - module Persistence - module Model - - # This module contains the storage related features of {Elasticsearch::Persistence::Model} - # - module Store - module ClassMethods #:nodoc: - - # Creates a class instance, saves it, if validations pass, and returns it - # - # @example Create a new person - # - # Person.create name: 'John Smith' - # # => # - # - # @return [Object] The model instance - # - def create(attributes, options={}) - object = self.new(attributes) - object.run_callbacks :create do - object.save(options) - object - end - end - end - - module InstanceMethods - - # Saves the model (if validations pass) and returns the response (or `false`) - # - # @example Save a valid model instance - # - # p = Person.new(name: 'John') - # p.save - # => {"_index"=>"people", ... "_id"=>"RzFSXFR0R8u1CZIWNs2Gvg", "_version"=>1, "created"=>true} - # - # @example Save an invalid model instance - # - # p = Person.new(name: nil) - # p.save - # # => false - # - # @return [Hash,FalseClass] The Elasticsearch response as a Hash or `false` - # - def save(options={}) - unless options.delete(:validate) == false - return false unless valid? - end - - run_callbacks :save do - options.update id: self.id - options.update index: self._index if self._index - options.update type: self._type if self._type - - self[:updated_at] = Time.now.utc - - response = self.class.gateway.save(self, options) - - @_id = response['_id'] - @_index = response['_index'] - @_type = response['_type'] - @_version = response['_version'] - @persisted = true - - response - end - end - - # Deletes the model from Elasticsearch (if it's persisted), freezes it, and returns the response - # - # @example Delete a model instance - # - # p.destroy - # => {"_index"=>"people", ... "_id"=>"RzFSXFR0R8u1CZIWNs2Gvg", "_version"=>2 ...} - # - # @return [Hash] The Elasticsearch response as a Hash - # - def destroy(options={}) - raise DocumentNotPersisted, "Object not persisted: #{self.inspect}" unless persisted? - - run_callbacks :destroy do - options.update index: self._index if self._index - options.update type: self._type if self._type - - response = self.class.gateway.delete(self.id, options) - - @destroyed = true - @persisted = false - self.freeze - response - end - end; alias :delete :destroy - - # Updates the model (via Elasticsearch's "Update" API) and returns the response - # - # @example Update a model with partial attributes - # - # p.update name: 'UPDATED' - # => {"_index"=>"people", ... "_version"=>2} - # - # @example Pass a version for concurrency control - # - # p.update( { name: 'UPDATED' }, { version: 2 } ) - # => {"_index"=>"people", ... "_version"=>3} - # - # @example An exception is raised when the version doesn't match - # - # p.update( { name: 'UPDATED' }, { version: 2 } ) - # => Elasticsearch::Transport::Transport::Errors::Conflict: [409] {"error" ... } - # - # @return [Hash] The Elasticsearch response as a Hash - # - def update(attributes={}, options={}) - unless options.delete(:validate) == false - return false unless valid? - end - raise DocumentNotPersisted, "Object not persisted: #{self.inspect}" unless persisted? - - run_callbacks :update do - options.update index: self._index if self._index - options.update type: self._type if self._type - - attributes.update( { updated_at: Time.now.utc } ) - response = self.class.gateway.update(self.id, { doc: attributes}.merge(options)) - - self.attributes = self.attributes.merge(attributes) - @_index = response['_index'] - @_type = response['_type'] - @_version = response['_version'] - - response - end - end; alias :update_attributes :update - - # Increments a numeric attribute (via Elasticsearch's "Update" API) and returns the response - # - # @example Increment the `salary` attribute by 1 - # - # p.increment :salary - # - # @example Increment the `salary` attribute by 100 - # - # p.increment :salary, 100 - # - # @return [Hash] The Elasticsearch response as a Hash - # - def increment(attribute, value=1, options={}) - raise DocumentNotPersisted, "Object not persisted: #{self.inspect}" unless persisted? - - options.update index: self._index if self._index - options.update type: self._type if self._type - - response = self.class.gateway.update(self.id, { script: "ctx._source.#{attribute} += #{value}"}.merge(options)) - - self[attribute] += value - - @_index = response['_index'] - @_type = response['_type'] - @_version = response['_version'] - - response - end - - # Decrements a numeric attribute (via Elasticsearch's "Update" API) and returns the response - # - # @example Decrement the `salary` attribute by 1 - # - # p.decrement :salary - # - # @example Decrement the `salary` attribute by 100 - # - # p.decrement :salary, 100 - # - # @return [Hash] The Elasticsearch response as a Hash - # - def decrement(attribute, value=1, options={}) - raise DocumentNotPersisted, "Object not persisted: #{self.inspect}" unless persisted? - - options.update index: self._index if self._index - options.update type: self._type if self._type - - response = self.class.gateway.update(self.id, { script: "ctx._source.#{attribute} = ctx._source.#{attribute} - #{value}"}.merge(options)) - self[attribute] -= value - - @_index = response['_index'] - @_type = response['_type'] - @_version = response['_version'] - - response - end - - # Updates the `updated_at` attribute, saves the model and returns the response - # - # @example Update the `updated_at` attribute (default) - # - # p.touch - # - # @example Update a custom attribute: `saved_on` - # - # p.touch :saved_on - # - # @return [Hash] The Elasticsearch response as a Hash - # - def touch(attribute=:updated_at, options={}) - raise DocumentNotPersisted, "Object not persisted: #{self.inspect}" unless persisted? - raise ArgumentError, "Object does not have '#{attribute}' attribute" unless respond_to?(attribute) - - run_callbacks :touch do - options.update index: self._index if self._index - options.update type: self._type if self._type - - value = Time.now.utc - response = self.class.gateway.update(self.id, { doc: { attribute => value.iso8601 }}.merge(options)) - - self[attribute] = value - - @_index = response['_index'] - @_type = response['_type'] - @_version = response['_version'] - - response - end - end - - # Returns true when the model has been destroyed, false otherwise - # - # @return [TrueClass,FalseClass] - # - def destroyed? - !!@destroyed - end - - # Returns true when the model has been already saved to the database, false otherwise - # - # @return [TrueClass,FalseClass] - # - def persisted? - !!@persisted && !destroyed? - end - - # Returns true when the model has not been saved yet, false otherwise - # - # @return [TrueClass,FalseClass] - # - def new_record? - !persisted? && !destroyed? - end - end - end - - end - end -end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/model/utils.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/model/utils.rb deleted file mode 100644 index e69de29bb..000000000 diff --git a/elasticsearch-persistence/lib/rails/generators/elasticsearch/model/model_generator.rb b/elasticsearch-persistence/lib/rails/generators/elasticsearch/model/model_generator.rb deleted file mode 100644 index 84fb632cd..000000000 --- a/elasticsearch-persistence/lib/rails/generators/elasticsearch/model/model_generator.rb +++ /dev/null @@ -1,21 +0,0 @@ -require "rails/generators/elasticsearch_generator" - -module Elasticsearch - module Generators - class ModelGenerator < ::Rails::Generators::NamedBase - source_root File.expand_path('../templates', __FILE__) - - desc "Creates an Elasticsearch::Persistence model" - argument :attributes, type: :array, default: [], banner: "attribute:type attribute:type" - - check_class_collision - - def create_model_file - @padding = attributes.map { |a| a.name.size }.max - template "model.rb.tt", File.join("app/models", class_path, "#{file_name}.rb") - end - - hook_for :test_framework - end - end -end diff --git a/elasticsearch-persistence/lib/rails/generators/elasticsearch/model/templates/model.rb.tt b/elasticsearch-persistence/lib/rails/generators/elasticsearch/model/templates/model.rb.tt deleted file mode 100644 index 0597174da..000000000 --- a/elasticsearch-persistence/lib/rails/generators/elasticsearch/model/templates/model.rb.tt +++ /dev/null @@ -1,9 +0,0 @@ -<% module_namespacing do -%> -class <%= class_name %> - include Elasticsearch::Persistence::Model - -<% attributes.each do |attribute| -%> - <%= "attribute :#{attribute.name},".ljust(@padding+12) %> <%= attribute.type %> -<% end -%> -end -<% end -%> diff --git a/elasticsearch-persistence/lib/rails/generators/elasticsearch_generator.rb b/elasticsearch-persistence/lib/rails/generators/elasticsearch_generator.rb deleted file mode 100644 index 20072ed93..000000000 --- a/elasticsearch-persistence/lib/rails/generators/elasticsearch_generator.rb +++ /dev/null @@ -1,2 +0,0 @@ -require "rails/generators/named_base" -require "rails/generators/active_model" diff --git a/elasticsearch-persistence/test/integration/model/model_basic_test.rb b/elasticsearch-persistence/test/integration/model/model_basic_test.rb deleted file mode 100644 index 33555c980..000000000 --- a/elasticsearch-persistence/test/integration/model/model_basic_test.rb +++ /dev/null @@ -1,233 +0,0 @@ -require 'test_helper' - -require 'elasticsearch/persistence/model' -require 'elasticsearch/persistence/model/rails' - -module Elasticsearch - module Persistence - class PersistenceModelBasicIntegrationTest < Elasticsearch::Test::IntegrationTestCase - - class ::Person - include Elasticsearch::Persistence::Model - include Elasticsearch::Persistence::Model::Rails - - settings index: { number_of_shards: 1 } - document_type 'human_being' - - attribute :name, String, - mapping: { fields: { - name: { type: 'text', analyzer: 'snowball' }, - raw: { type: 'keyword' } - } } - - attribute :birthday, Date - attribute :department, String - attribute :salary, Integer - attribute :admin, Boolean, default: false - - validates :name, presence: true - end - - context "A basic persistence model" do - setup do - Person.create_index! force: true - end - - should "save the object with custom ID" do - person = Person.new id: 1, name: 'Number One' - person.save - - document = Person.find(1) - assert_not_nil document - assert_equal 'Number One', document.name - end - - should "create the object with custom ID" do - person = Person.create id: 1, name: 'Number One' - - document = Person.find(1) - assert_not_nil document - assert_equal 'Number One', document.name - end - - should "save and find the object" do - person = Person.new name: 'John Smith', birthday: Date.parse('1970-01-01') - assert person.save - - assert_not_nil person.id - document = Person.find(person.id) - - assert_instance_of Person, document - assert_equal 'John Smith', document.name - assert_equal 'John Smith', Person.find(person.id).name - - assert_not_nil Elasticsearch::Persistence.client.get index: 'people', type: 'human_being', id: person.id - end - - should "not save an invalid object" do - person = Person.new name: nil - assert ! person.save - end - - should "save an invalid object with the :validate option" do - person = Person.new name: nil, salary: 100 - assert person.save validate: false - - assert_not_nil person.id - document = Person.find(person.id) - assert_equal 100, document.salary - end - - should "delete the object" do - person = Person.create name: 'John Smith', birthday: Date.parse('1970-01-01') - - person.destroy - assert person.frozen? - - assert_raise Elasticsearch::Transport::Transport::Errors::NotFound do - Elasticsearch::Persistence.client.get index: 'people', type: 'person', id: person.id - end - end - - should "update an object attribute" do - person = Person.create name: 'John Smith' - - person.update name: 'UPDATED' - - assert_equal 'UPDATED', person.name - assert_equal 'UPDATED', Person.find(person.id).name - end - - should "create the model with correct Date form Rails' form attributes" do - params = { "birthday(1i)"=>"2014", - "birthday(2i)"=>"1", - "birthday(3i)"=>"1" - } - person = Person.create params.merge(name: 'TEST') - - assert_equal Date.parse('2014-01-01'), person.birthday - assert_equal Date.parse('2014-01-01'), Person.find(person.id).birthday - end - - should_eventually "update the model with correct Date form Rails' form attributes" do - params = { "birthday(1i)"=>"2014", - "birthday(2i)"=>"1", - "birthday(3i)"=>"1" - } - person = Person.create params.merge(name: 'TEST') - - person.update params.merge('birthday(1i)' => '2015') - - assert_equal Date.parse('2015-01-01'), person.birthday - assert_equal Date.parse('2015-01-01'), Person.find(person.id).birthday - end - - should "increment an object attribute" do - person = Person.create name: 'John Smith', salary: 1_000 - - person.increment :salary - - assert_equal 1_001, person.salary - assert_equal 1_001, Person.find(person.id).salary - end - - should "update the object timestamp" do - person = Person.create name: 'John Smith' - updated_at = person.updated_at - - sleep 1 - person.touch - - assert person.updated_at > updated_at, [person.updated_at, updated_at].inspect - - found = Person.find(person.id) - assert found.updated_at > updated_at, [found.updated_at, updated_at].inspect - end - - should 'update the object timestamp on save' do - person = Person.create name: 'John Smith' - person.admin = true - sleep 1 - person.save - - Person.gateway.refresh_index! - - found = Person.find(person.id) - - # Compare without milliseconds - assert_equal person.updated_at.to_i, found.updated_at.to_i - end - - should "respect the version" do - person = Person.create name: 'John Smith' - - person.update( { name: 'UPDATE 1' }) - - assert_raise Elasticsearch::Transport::Transport::Errors::Conflict do - person.update( { name: 'UPDATE 2' }, { version: 1 } ) - end - end - - should "find all instances" do - Person.create name: 'John Smith' - Person.create name: 'Mary Smith' - Person.gateway.refresh_index! - - people = Person.all - - assert_equal 2, people.total - assert_equal 2, people.size - end - - should "find instances by search" do - Person.create name: 'John Smith' - Person.create name: 'Mary Smith' - Person.gateway.refresh_index! - - people = Person.search query: { match: { name: 'smith' } }, - highlight: { fields: { name: {} } } - - assert_equal 2, people.total - assert_equal 2, people.size - - assert people.map_with_hit { |o,h| h._score }.all? { |s| s > 0 } - - assert_not_nil people.first.hit - assert_match /smith/i, people.first.hit.highlight['name'].first - end - - should "find instances in batches" do - 50.times { |i| Person.create name: "John #{i+1}" } - Person.gateway.refresh_index! - - @batches = 0 - @results = [] - - Person.find_in_batches(_source_include: 'name') do |batch| - @batches += 1 - @results += batch.map(&:name) - end - - assert_equal 3, @batches - assert_equal 50, @results.size - assert_contains @results, 'John 1' - end - - should "find each instance" do - 50.times { |i| Person.create name: "John #{i+1}" } - Person.gateway.refresh_index! - - @results = [] - - Person.find_each(_source_include: 'name') do |person| - @results << person.name - end - - assert_equal 50, @results.size - assert_contains @results, 'John 1' - end - end - - end - end -end diff --git a/elasticsearch-persistence/test/unit/model_base_test.rb b/elasticsearch-persistence/test/unit/model_base_test.rb deleted file mode 100644 index 2d55e1620..000000000 --- a/elasticsearch-persistence/test/unit/model_base_test.rb +++ /dev/null @@ -1,72 +0,0 @@ -require 'test_helper' - -require 'elasticsearch/persistence/model' -require 'elasticsearch/persistence/model/rails' - -class Elasticsearch::Persistence::ModelBaseTest < Test::Unit::TestCase - context "The model" do - setup do - class DummyBaseModel - include Elasticsearch::Persistence::Model - - attribute :name, String - end - end - - should "respond to id, _id, _index, _type and _version" do - model = DummyBaseModel.new - - [:id, :_id, :_index, :_type, :_version].each { |method| assert_respond_to model, method } - end - - should "set the ID from attributes during initialization" do - model = DummyBaseModel.new id: 1 - assert_equal 1, model.id - - model = DummyBaseModel.new 'id' => 2 - assert_equal 2, model.id - end - - should "set the ID using setter method" do - model = DummyBaseModel.new id: 1 - assert_equal 1, model.id - - model.id = 2 - assert_equal 2, model.id - end - - should "have ID in attributes" do - model = DummyBaseModel.new id: 1, name: 'Test' - assert_equal 1, model.attributes[:id] - end - - should "have the customized inspect method" do - model = DummyBaseModel.new name: 'Test' - assert_match /name\: "Test"/, model.inspect - end - - context "with custom document_type" do - setup do - @model = DummyBaseModel - @gateway = mock() - @mapping = mock() - @model.stubs(:gateway).returns(@gateway) - @gateway.stubs(:mapping).returns(@mapping) - @document_type = 'dummybase' - end - - should "forward the argument to mapping" do - @gateway.expects(:document_type).with(@document_type).once - @mapping.expects(:type=).with(@document_type).once - @model.document_type @document_type - end - - should "return the value from the gateway" do - @gateway.expects(:document_type).once.returns(@document_type) - @mapping.expects(:type=).never - returned_type = @model.document_type - assert_equal @document_type, returned_type - end - end - end -end diff --git a/elasticsearch-persistence/test/unit/model_find_test.rb b/elasticsearch-persistence/test/unit/model_find_test.rb deleted file mode 100644 index 92370c1e4..000000000 --- a/elasticsearch-persistence/test/unit/model_find_test.rb +++ /dev/null @@ -1,153 +0,0 @@ -require 'test_helper' - -require 'active_model' -require 'virtus' - -require 'elasticsearch/persistence/model/errors' -require 'elasticsearch/persistence/model/find' - -class Elasticsearch::Persistence::ModelFindTest < Test::Unit::TestCase - context "The model find module," do - - class DummyFindModel - include ActiveModel::Naming - include ActiveModel::Conversion - include ActiveModel::Serialization - include ActiveModel::Serializers::JSON - include ActiveModel::Validations - - include Virtus.model - - extend Elasticsearch::Persistence::Model::Find::ClassMethods - - extend ActiveModel::Callbacks - define_model_callbacks :create, :save, :update, :destroy - define_model_callbacks :find, :touch, only: :after - - attribute :title, String - attribute :count, Integer, default: 0 - attribute :created_at, DateTime, default: lambda { |o,a| Time.now.utc } - attribute :updated_at, DateTime, default: lambda { |o,a| Time.now.utc } - end - - setup do - @gateway = stub(client: stub(), index_name: 'foo', document_type: 'bar') - DummyFindModel.stubs(:gateway).returns(@gateway) - - DummyFindModel.stubs(:index_name).returns('foo') - DummyFindModel.stubs(:document_type).returns('bar') - - @response = MultiJson.load <<-JSON - { - "took": 14, - "timed_out": false, - "_shards": { - "total": 5, - "successful": 5, - "failed": 0 - }, - "hits": { - "total": 1, - "max_score": 1.0, - "hits": [ - { - "_index": "dummy", - "_type": "dummy", - "_id": "abc123", - "_score": 1.0, - "_source": { - "name": "TEST" - } - } - ] - } - } - JSON - end - - should "find all records" do - DummyFindModel - .stubs(:search) - .with({ query: { match_all: {} }, size: 10_000 }, {}) - .returns(@response) - - DummyFindModel.all - end - - should "pass options when finding all records" do - DummyFindModel - .expects(:search) - .with({ query: { match: { title: 'test' } }, size: 10_000 }, { routing: 'abc123' }) - .returns(@response) - - DummyFindModel.all( { query: { match: { title: 'test' } } }, { routing: 'abc123' } ) - end - - context "finding via scroll" do - setup do - @gateway - .expects(:deserialize) - .returns('_source' => {'foo' => 'bar'}) - .at_least_once - - # 1. Initial batch of documents and the scroll_id - @gateway.client - .expects(:search) - .with do |arguments| - assert_equal 'foo', arguments[:index] - assert_equal 'bar', arguments[:type] - true - end - .returns(MultiJson.load('{"_scroll_id":"abc123==", "hits":{"hits":[{"_source":{"foo":"bar_1"}}]}}')) - - # 2. Second batch of documents and the scroll_id - # 3. Last, empty batch of documents - @gateway.client - .expects(:scroll) - .twice - .returns(MultiJson.load('{"_scroll_id":"abc456==", "hits":{"hits":[{"_source":{"foo":"bar_2"}}]}}')) - .then - .returns(MultiJson.load('{"_scroll_id":"abc789==", "hits":{"hits":[]}}')) - end - - should "find all records in batches" do - @doc = nil - result = DummyFindModel.find_in_batches { |batch| @doc = batch.first['_source']['foo'] } - - assert_equal 'abc789==', result - assert_equal 'bar', @doc - end - - should "return an Enumerator for find in batches" do - @doc = nil - assert_nothing_raised do - e = DummyFindModel.find_in_batches - assert_instance_of Enumerator, e - - e.each { |batch| @doc = batch.first['_source']['foo'] } - assert_equal 'bar', @doc - end - end - - should "find each" do - @doc = nil - result = DummyFindModel.find_each { |doc| @doc = doc['_source']['foo'] } - - assert_equal 'abc789==', result - assert_equal 'bar', @doc - end - - should "return an Enumerator for find each" do - @doc = nil - assert_nothing_raised do - e = DummyFindModel.find_each - assert_instance_of Enumerator, e - - e.each { |doc| @doc = doc['_source']['foo'] } - assert_equal 'bar', @doc - end - end - end - - end -end diff --git a/elasticsearch-persistence/test/unit/model_gateway_test.rb b/elasticsearch-persistence/test/unit/model_gateway_test.rb deleted file mode 100644 index 2451241d6..000000000 --- a/elasticsearch-persistence/test/unit/model_gateway_test.rb +++ /dev/null @@ -1,101 +0,0 @@ -require 'test_helper' - -require 'elasticsearch/persistence/model' -require 'elasticsearch/persistence/model/rails' - -class Elasticsearch::Persistence::ModelGatewayTest < Test::Unit::TestCase - context "The model gateway" do - setup do - class DummyGatewayModel - include Elasticsearch::Persistence::Model - end - end - - teardown do - Elasticsearch::Persistence::ModelGatewayTest.__send__ :remove_const, :DummyGatewayModel \ - rescue NameError; nil - end - - should "be accessible" do - assert_instance_of Elasticsearch::Persistence::Repository::Class, DummyGatewayModel.gateway - - $a = 0 - DummyGatewayModel.gateway { $a += 1 } - assert_equal 1, $a - - @b = 0 - def run!; DummyGatewayModel.gateway { |g| @b += 1 }; end - run! - assert_equal 1, @b - - assert_equal DummyGatewayModel, DummyGatewayModel.gateway.klass - end - - should "define common attributes" do - d = DummyGatewayModel.new - - assert_respond_to d, :updated_at - assert_respond_to d, :created_at - end - - should "allow to configure settings" do - DummyGatewayModel.settings(number_of_shards: 1) - - assert_equal 1, DummyGatewayModel.settings.to_hash[:number_of_shards] - end - - should "allow to configure mappings" do - DummyGatewayModel.mapping { indexes :name, analyzer: 'snowball' } - - assert_equal 'snowball', - DummyGatewayModel.mapping.to_hash[:dummy_gateway_model][:properties][:name][:analyzer] - end - - should "configure the mapping via attribute" do - DummyGatewayModel.attribute :name, String, mapping: { analyzer: 'snowball' } - - assert_respond_to DummyGatewayModel, :name - assert_equal 'snowball', - DummyGatewayModel.mapping.to_hash[:dummy_gateway_model][:properties][:name][:analyzer] - end - - should "configure the mapping via an attribute block" do - DummyGatewayModel.attribute :name, String do - indexes :name, analyzer: 'custom' - end - - assert_respond_to DummyGatewayModel, :name - assert_equal 'custom', - DummyGatewayModel.mapping.to_hash[:dummy_gateway_model][:properties][:name][:analyzer] - end - - should "properly look up types for classes" do - assert_equal 'text', Elasticsearch::Persistence::Model::Utils::lookup_type(String) - assert_equal 'integer', Elasticsearch::Persistence::Model::Utils::lookup_type(Integer) - assert_equal 'float', Elasticsearch::Persistence::Model::Utils::lookup_type(Float) - assert_equal 'date', Elasticsearch::Persistence::Model::Utils::lookup_type(Date) - assert_equal 'boolean', Elasticsearch::Persistence::Model::Utils::lookup_type(Virtus::Attribute::Boolean) - end - - should "remove IDs from hash when serializing" do - assert_equal( {foo: 'bar'}, DummyGatewayModel.gateway.serialize(id: '123', foo: 'bar') ) - end - - should "set IDs from hash when deserializing" do - assert_equal 'abc123', DummyGatewayModel.gateway.deserialize('_id' => 'abc123', '_source' => {}).id - end - - should "set @persisted variable from hash when deserializing" do - assert DummyGatewayModel.gateway.deserialize('_id' => 'abc123', '_source' => {}).instance_variable_get(:@persisted) - end - - should "allow accessing the raw _source" do - assert_equal 'bar', DummyGatewayModel.gateway.deserialize('_source' => { 'foo' => 'bar' })._source['foo'] - end - - should "allow to access the raw hit from results as Hashie::Mash" do - assert_equal 0.42, DummyGatewayModel.gateway.deserialize('_score' => 0.42, '_source' => {}).hit._score - end - - end -end diff --git a/elasticsearch-persistence/test/unit/model_rails_test.rb b/elasticsearch-persistence/test/unit/model_rails_test.rb deleted file mode 100644 index 3f0ce913b..000000000 --- a/elasticsearch-persistence/test/unit/model_rails_test.rb +++ /dev/null @@ -1,112 +0,0 @@ -require 'test_helper' - -require 'elasticsearch/persistence/model' -require 'elasticsearch/persistence/model/rails' - -require 'rails' -require 'action_controller/railtie' -require 'action_view/railtie' - -class ::MyRailsModel - include Elasticsearch::Persistence::Model - include Elasticsearch::Persistence::Model::Rails - - attribute :name, String, mapping: { analyzer: 'english' } - attribute :published_at, DateTime - attribute :published_on, Date -end - -class Application < Rails::Application - config.eager_load = false - config.root = File.dirname(File.expand_path('../../../tmp', __FILE__)) - config.logger = Logger.new($stderr) - - routes.append do - resources :my_rails_models - end -end - -class ApplicationController < ActionController::Base - include Application.routes.url_helpers - include ActionController::UrlFor -end -ApplicationController.default_url_options = { host: 'localhost' } -ApplicationController._routes.append { resources :my_rails_models } - -class MyRailsModelController < ApplicationController; end - -Application.initialize! - -class Elasticsearch::Persistence::ModelRailsTest < Test::Unit::TestCase - context "The model in a Rails application" do - - should "generate proper URLs and paths" do - model = MyRailsModel.new name: 'Test' - model.stubs(:id).returns(1) - model.stubs(:persisted?).returns(true) - - controller = MyRailsModelController.new - controller.request = ActionDispatch::Request.new({}) - - assert_equal 'http://localhost/my_rails_models/1', controller.url_for(model) - assert_equal '/my_rails_models/1/edit', controller.edit_my_rails_model_path(model) - end - - should "generate a link" do - class MyView; include ActionView::Helpers::UrlHelper; end - - model = MyRailsModel.new name: 'Test' - view = MyView.new - view.expects(:url_for).with(model).returns('foo/bar') - - assert_equal 'Show', view.link_to('Show', model) - end - - should "parse DateTime from Rails forms" do - params = { "published_at(1i)"=>"2014", - "published_at(2i)"=>"1", - "published_at(3i)"=>"1", - "published_at(4i)"=>"12", - "published_at(5i)"=>"00" - } - - assert_equal '2014-1-1- 12:00:', - Elasticsearch::Persistence::Model::Rails.__convert_rails_dates(params)['published_at'] - - m = MyRailsModel.new params - assert_equal "2014-01-01T12:00:00+00:00", m.published_at.iso8601 - end - - should "parse Date from Rails forms" do - params = { "published_on(1i)"=>"2014", - "published_on(2i)"=>"1", - "published_on(3i)"=>"1" - } - - assert_equal '2014-1-1-', - Elasticsearch::Persistence::Model::Rails.__convert_rails_dates(params)['published_on'] - - - m = MyRailsModel.new params - assert_equal "2014-01-01", m.published_on.iso8601 - end - - context "when updating," do - should "pass the options to gateway" do - model = MyRailsModel.new name: 'Test' - model.stubs(:persisted?).returns(true) - - model.class.gateway - .expects(:update) - .with do |object, options| - assert_equal 'ABC', options[:routing] - true - end - .returns({'_id' => 'abc123'}) - - assert model.update( { title: 'UPDATED' }, { routing: 'ABC' } ) - end - end - - end -end diff --git a/elasticsearch-persistence/test/unit/model_store_test.rb b/elasticsearch-persistence/test/unit/model_store_test.rb deleted file mode 100644 index 442ab56ee..000000000 --- a/elasticsearch-persistence/test/unit/model_store_test.rb +++ /dev/null @@ -1,576 +0,0 @@ -require 'test_helper' - -require 'active_model' -require 'virtus' - -require 'elasticsearch/persistence/model/base' -require 'elasticsearch/persistence/model/errors' -require 'elasticsearch/persistence/model/store' - -class Elasticsearch::Persistence::ModelStoreTest < Test::Unit::TestCase - context "The model store module," do - - class DummyStoreModel - include ActiveModel::Naming - include ActiveModel::Conversion - include ActiveModel::Serialization - include ActiveModel::Serializers::JSON - include ActiveModel::Validations - - include Virtus.model - - include Elasticsearch::Persistence::Model::Base::InstanceMethods - extend Elasticsearch::Persistence::Model::Store::ClassMethods - include Elasticsearch::Persistence::Model::Store::InstanceMethods - - extend ActiveModel::Callbacks - define_model_callbacks :create, :save, :update, :destroy - define_model_callbacks :find, :touch, only: :after - - attribute :title, String - attribute :count, Integer, default: 0 - attribute :created_at, DateTime, default: lambda { |o,a| Time.now.utc } - attribute :updated_at, DateTime, default: lambda { |o,a| Time.now.utc } - end - - setup do - @shoulda_subject = DummyStoreModel.new title: 'Test' - @gateway = stub - DummyStoreModel.stubs(:gateway).returns(@gateway) - end - - teardown do - Elasticsearch::Persistence::ModelStoreTest.__send__ :remove_const, :DummyStoreModelWithCallback \ - rescue NameError; nil - end - - should "be new_record" do - assert subject.new_record? - end - - context "when creating," do - should "save the object and return it" do - DummyStoreModel.any_instance.expects(:save).returns({'_id' => 'X'}) - - assert_instance_of DummyStoreModel, DummyStoreModel.create(title: 'Test') - end - - should "execute the callbacks" do - DummyStoreModelWithCallback = Class.new(DummyStoreModel) - @gateway.expects(:save).returns({'_id' => 'X'}) - - DummyStoreModelWithCallback.after_create { $stderr.puts "CREATED" } - DummyStoreModelWithCallback.after_save { $stderr.puts "SAVED" } - - $stderr.expects(:puts).with('CREATED') - $stderr.expects(:puts).with('SAVED') - - DummyStoreModelWithCallback.create name: 'test' - end - end - - context "when saving," do - should "save the model" do - @gateway - .expects(:save) - .with do |object, options| - assert_equal subject, object - assert_equal nil, options[:id] - true - end - .returns({'_id' => 'abc123'}) - - assert subject.save - end - - should "save the model and set the ID" do - @gateway - .expects(:save) - .returns({'_id' => 'abc123'}) - - assert_nil subject.id - - subject.save - assert_equal 'abc123', subject.id - end - - should "save the model and update the timestamp" do - now = Time.parse('2014-01-01T00:00:00Z') - Time.expects(:now).returns(now).at_least_once - @gateway - .expects(:save) - .returns({'_id' => 'abc123'}) - - subject.save - assert_equal Time.parse('2014-01-01T00:00:00Z'), subject.updated_at - end - - should "not save an invalid model" do - @gateway - .expects(:save) - .never - - subject.instance_eval do - def valid?; false; end; - end - - assert ! subject.save - assert ! subject.persisted? - end - - should "skip the validation with the :validate option" do - @gateway - .expects(:save) - .with do |object, options| - assert_equal subject, object - assert_equal nil, options[:id] - true - end - .returns({'_id' => 'abc123'}) - - subject.instance_eval do - def valid?; false; end; - end - - assert subject.save validate: false - assert subject.persisted? - end - - should "pass the options to gateway" do - @gateway - .expects(:save) - .with do |object, options| - assert_equal 'ABC', options[:routing] - true - end - .returns({'_id' => 'abc123'}) - - assert subject.save routing: 'ABC' - end - - should "return the response" do - @gateway - .expects(:save) - .returns('FOOBAR') - - assert_equal 'FOOBAR', subject.save - end - - should "execute the callbacks" do - @gateway.expects(:save).returns({'_id' => 'abc'}) - DummyStoreModelWithCallback = Class.new(DummyStoreModel) - - DummyStoreModelWithCallback.after_save { $stderr.puts "SAVED" } - - $stderr.expects(:puts).with('SAVED') - d = DummyStoreModelWithCallback.new name: 'Test' - d.save - end - - should "save the model to its own index" do - @gateway.expects(:save) - .with do |model, options| - assert_equal 'my_custom_index', options[:index] - assert_equal 'my_custom_type', options[:type] - true - end - .returns({'_id' => 'abc'}) - - d = DummyStoreModel.new name: 'Test' - d.instance_variable_set(:@_index, 'my_custom_index') - d.instance_variable_set(:@_type, 'my_custom_type') - d.save - end - - should "set the meta attributes from response" do - @gateway.expects(:save) - .with do |model, options| - assert_equal 'my_custom_index', options[:index] - assert_equal 'my_custom_type', options[:type] - true - end - .returns({'_id' => 'abc', '_index' => 'foo', '_type' => 'bar', '_version' => '100'}) - - d = DummyStoreModel.new name: 'Test' - d.instance_variable_set(:@_index, 'my_custom_index') - d.instance_variable_set(:@_type, 'my_custom_type') - d.save - - assert_equal 'foo', d._index - assert_equal 'bar', d._type - assert_equal '100', d._version - end - end - - context "when destroying," do - should "remove the model from Elasticsearch" do - subject.expects(:persisted?).returns(true) - subject.expects(:id).returns('abc123') - subject.expects(:freeze).returns(subject) - - @gateway - .expects(:delete) - .with('abc123', {}) - .returns({'_id' => 'abc123', 'version' => 2}) - - assert subject.destroy - assert subject.destroyed? - end - - should "pass the options to gateway" do - subject.expects(:persisted?).returns(true) - subject.expects(:freeze).returns(subject) - - @gateway - .expects(:delete) - .with do |object, options| - assert_equal 'ABC', options[:routing] - true - end - .returns({'_id' => 'abc123'}) - - assert subject.destroy routing: 'ABC' - end - - should "return the response" do - subject.expects(:persisted?).returns(true) - subject.expects(:freeze).returns(subject) - - @gateway - .expects(:delete) - .returns('FOOBAR') - - assert_equal 'FOOBAR', subject.destroy - end - - should "execute the callbacks" do - @gateway.expects(:delete).returns({'_id' => 'abc'}) - DummyStoreModelWithCallback = Class.new(DummyStoreModel) - - DummyStoreModelWithCallback.after_destroy { $stderr.puts "DELETED" } - - $stderr.expects(:puts).with('DELETED') - d = DummyStoreModelWithCallback.new name: 'Test' - d.expects(:persisted?).returns(true) - d.expects(:freeze).returns(d) - - d.destroy - end - - should "remove the model from its own index" do - @gateway.expects(:delete) - .with do |model, options| - assert_equal 'my_custom_index', options[:index] - assert_equal 'my_custom_type', options[:type] - true - end - .returns({'_id' => 'abc'}) - - d = DummyStoreModel.new name: 'Test' - d.instance_variable_set(:@_index, 'my_custom_index') - d.instance_variable_set(:@_type, 'my_custom_type') - d.expects(:persisted?).returns(true) - d.expects(:freeze).returns(d) - - d.destroy - end - end - - context "when updating," do - should "update the document with partial attributes" do - subject.expects(:persisted?).returns(true) - subject.expects(:id).returns('abc123').at_least_once - - @gateway - .expects(:update) - .with do |id, options| - assert_equal 'abc123', id - assert_equal 'UPDATED', options[:doc][:title] - true - end - .returns({'_id' => 'abc123', 'version' => 2}) - - assert subject.update title: 'UPDATED' - - assert_equal 'UPDATED', subject.title - end - - should "allow to update the document with a custom script" do - subject.expects(:persisted?).returns(true) - subject.expects(:id).returns('abc123').at_least_once - - @gateway - .expects(:update) - .with do |id, options| - assert_equal 'abc123', id - assert_equal 'EXEC', options[:script] - true - end - .returns({'_id' => 'abc123', 'version' => 2}) - - assert subject.update( {}, { script: 'EXEC' } ) - end - - should "not update an invalid model" do - @gateway - .expects(:update) - .never - - subject.instance_eval do - def valid?; false; end; - end - - assert ! subject.update(title: 'INVALID') - end - - should "skip the validation with the :validate option" do - subject.expects(:persisted?).returns(true).at_least_once - subject.expects(:id).returns('abc123').at_least_once - - @gateway - .expects(:update) - .with do |object, options| - assert_equal 'abc123', object - assert_equal nil, options[:id] - assert_equal 'INVALID', options[:doc][:title] - true - end - .returns({'_id' => 'abc123'}) - - subject.instance_eval do - def valid?; false; end; - end - - assert subject.update( { title: 'INVALID' }, { validate: false } ) - assert subject.persisted? - end - - should "pass the options to gateway" do - subject.expects(:persisted?).returns(true) - - @gateway - .expects(:update) - .with do |object, options| - assert_equal 'ABC', options[:routing] - true - end - .returns({'_id' => 'abc123'}) - - assert subject.update( { title: 'UPDATED' }, { routing: 'ABC' } ) - end - - should "return the response" do - subject.expects(:persisted?).returns(true) - - @gateway - .expects(:update) - .returns('FOOBAR') - - assert_equal 'FOOBAR', subject.update - end - - should "execute the callbacks" do - @gateway.expects(:update).returns({'_id' => 'abc'}) - DummyStoreModelWithCallback = Class.new(DummyStoreModel) - - DummyStoreModelWithCallback.after_update { $stderr.puts "UPDATED" } - - $stderr.expects(:puts).with('UPDATED') - d = DummyStoreModelWithCallback.new name: 'Test' - d.expects(:persisted?).returns(true) - d.update name: 'Update' - end - - should "update the model in its own index" do - @gateway.expects(:update) - .with do |model, options| - assert_equal 'my_custom_index', options[:index] - assert_equal 'my_custom_type', options[:type] - true - end - .returns({'_id' => 'abc'}) - - d = DummyStoreModel.new name: 'Test' - d.instance_variable_set(:@_index, 'my_custom_index') - d.instance_variable_set(:@_type, 'my_custom_type') - d.expects(:persisted?).returns(true) - - d.update name: 'Update' - end - - should "set the meta attributes from response" do - @gateway.expects(:update) - .with do |model, options| - assert_equal 'my_custom_index', options[:index] - assert_equal 'my_custom_type', options[:type] - true - end - .returns({'_id' => 'abc', '_index' => 'foo', '_type' => 'bar', '_version' => '100'}) - - d = DummyStoreModel.new name: 'Test' - d.instance_variable_set(:@_index, 'my_custom_index') - d.instance_variable_set(:@_type, 'my_custom_type') - d.expects(:persisted?).returns(true) - - d.update name: 'Update' - - assert_equal 'foo', d._index - assert_equal 'bar', d._type - assert_equal '100', d._version - end - end - - context "when incrementing," do - should "increment the attribute" do - subject.expects(:persisted?).returns(true) - - @gateway - .expects(:update) - .with do |id, options| - assert_equal 'ctx._source.count += 1', options[:script] - true - end - .returns({'_id' => 'abc123', 'version' => 2}) - - assert subject.increment :count - - assert_equal 1, subject.count - end - - should "set the meta attributes from response" do - subject.expects(:persisted?).returns(true) - - @gateway.expects(:update) - .with do |model, options| - assert_equal 'my_custom_index', options[:index] - assert_equal 'my_custom_type', options[:type] - true - end - .returns({'_id' => 'abc', '_index' => 'foo', '_type' => 'bar', '_version' => '100'}) - - subject.instance_variable_set(:@_index, 'my_custom_index') - subject.instance_variable_set(:@_type, 'my_custom_type') - - subject.increment :count - - assert_equal 'foo', subject._index - assert_equal 'bar', subject._type - assert_equal '100', subject._version - end - end - - context "when decrement," do - should "decrement the attribute" do - subject.expects(:persisted?).returns(true) - - @gateway - .expects(:update) - .with do |id, options| - assert_equal 'ctx._source.count = ctx._source.count - 1', options[:script] - true - end - .returns({'_id' => 'abc123', 'version' => 2}) - - assert subject.decrement :count - - assert_equal -1, subject.count - end - - should "set the meta attributes from response" do - subject.expects(:persisted?).returns(true) - - @gateway.expects(:update) - .with do |model, options| - assert_equal 'my_custom_index', options[:index] - assert_equal 'my_custom_type', options[:type] - true - end - .returns({'_id' => 'abc', '_index' => 'foo', '_type' => 'bar', '_version' => '100'}) - - subject.instance_variable_set(:@_index, 'my_custom_index') - subject.instance_variable_set(:@_type, 'my_custom_type') - - subject.decrement :count - - assert_equal 'foo', subject._index - assert_equal 'bar', subject._type - assert_equal '100', subject._version - end - end - - context "when touching," do - should "raise exception when touching not existing attribute" do - subject.expects(:persisted?).returns(true) - assert_raise(ArgumentError) { subject.touch :foobar } - end - - should "update updated_at by default" do - subject.expects(:persisted?).returns(true) - now = Time.parse('2014-01-01T00:00:00Z') - Time.expects(:now).returns(now).at_least_once - - @gateway - .expects(:update) - .with do |id, options| - assert_equal '2014-01-01T00:00:00Z', options[:doc][:updated_at] - true - end - .returns({'_id' => 'abc123', 'version' => 2}) - - subject.touch - assert_equal Time.parse('2014-01-01T00:00:00Z'), subject.updated_at - end - - should "update a custom attribute by default" do - subject.expects(:persisted?).returns(true) - now = Time.parse('2014-01-01T00:00:00Z') - Time.expects(:now).returns(now).at_least_once - - @gateway - .expects(:update) - .with do |id, options| - assert_equal '2014-01-01T00:00:00Z', options[:doc][:created_at] - true - end - .returns({'_id' => 'abc123', 'version' => 2}) - - subject.touch :created_at - assert_equal Time.parse('2014-01-01T00:00:00Z'), subject.created_at - end - - should "execute the callbacks" do - @gateway.expects(:update).returns({'_id' => 'abc'}) - DummyStoreModelWithCallback = Class.new(DummyStoreModel) - - DummyStoreModelWithCallback.after_touch { $stderr.puts "TOUCHED" } - - $stderr.expects(:puts).with('TOUCHED') - d = DummyStoreModelWithCallback.new name: 'Test' - d.expects(:persisted?).returns(true) - d.touch - end - - should "set the meta attributes from response" do - subject.expects(:persisted?).returns(true) - - @gateway.expects(:update) - .with do |model, options| - assert_equal 'my_custom_index', options[:index] - assert_equal 'my_custom_type', options[:type] - true - end - .returns({'_id' => 'abc', '_index' => 'foo', '_type' => 'bar', '_version' => '100'}) - - subject.instance_variable_set(:@_index, 'my_custom_index') - subject.instance_variable_set(:@_type, 'my_custom_type') - - subject.touch - - assert_equal 'foo', subject._index - assert_equal 'bar', subject._type - assert_equal '100', subject._version - end - end - - end -end diff --git a/elasticsearch-rails/lib/elasticsearch/rails/instrumentation/railtie.rb b/elasticsearch-rails/lib/elasticsearch/rails/instrumentation/railtie.rb index dbcd0fc38..6aeb6a866 100644 --- a/elasticsearch-rails/lib/elasticsearch/rails/instrumentation/railtie.rb +++ b/elasticsearch-rails/lib/elasticsearch/rails/instrumentation/railtie.rb @@ -16,10 +16,6 @@ class Railtie < ::Rails::Railtie include Elasticsearch::Rails::Instrumentation::Publishers::SearchRequest end if defined?(Elasticsearch::Model::Searching::SearchRequest) - Elasticsearch::Persistence::Model::Find::SearchRequest.class_eval do - include Elasticsearch::Rails::Instrumentation::Publishers::SearchRequest - end if defined?(Elasticsearch::Persistence::Model::Find::SearchRequest) - ActiveSupport.on_load(:action_controller) do include Elasticsearch::Rails::Instrumentation::ControllerRuntime end diff --git a/elasticsearch-rails/lib/elasticsearch/rails/lograge.rb b/elasticsearch-rails/lib/elasticsearch/rails/lograge.rb index a8edd8084..aa0f4f1f3 100644 --- a/elasticsearch-rails/lib/elasticsearch/rails/lograge.rb +++ b/elasticsearch-rails/lib/elasticsearch/rails/lograge.rb @@ -25,10 +25,6 @@ class Railtie < ::Rails::Railtie include Elasticsearch::Rails::Instrumentation::Publishers::SearchRequest end if defined?(Elasticsearch::Model::Searching::SearchRequest) - Elasticsearch::Persistence::Model::Find::SearchRequest.class_eval do - include Elasticsearch::Rails::Instrumentation::Publishers::SearchRequest - end if defined?(Elasticsearch::Persistence::Model::Find::SearchRequest) - ActiveSupport.on_load(:action_controller) do include Elasticsearch::Rails::Instrumentation::ControllerRuntime end