elasticsearch-persistence 0.0.0 → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +15 -0
  2. data/LICENSE.txt +10 -19
  3. data/README.md +432 -14
  4. data/Rakefile +56 -0
  5. data/elasticsearch-persistence.gemspec +45 -17
  6. data/examples/sinatra/.gitignore +7 -0
  7. data/examples/sinatra/Gemfile +28 -0
  8. data/examples/sinatra/README.markdown +36 -0
  9. data/examples/sinatra/application.rb +238 -0
  10. data/examples/sinatra/config.ru +7 -0
  11. data/examples/sinatra/test.rb +118 -0
  12. data/lib/elasticsearch/persistence.rb +88 -2
  13. data/lib/elasticsearch/persistence/client.rb +51 -0
  14. data/lib/elasticsearch/persistence/repository.rb +75 -0
  15. data/lib/elasticsearch/persistence/repository/class.rb +71 -0
  16. data/lib/elasticsearch/persistence/repository/find.rb +73 -0
  17. data/lib/elasticsearch/persistence/repository/naming.rb +115 -0
  18. data/lib/elasticsearch/persistence/repository/response/results.rb +90 -0
  19. data/lib/elasticsearch/persistence/repository/search.rb +60 -0
  20. data/lib/elasticsearch/persistence/repository/serialize.rb +31 -0
  21. data/lib/elasticsearch/persistence/repository/store.rb +95 -0
  22. data/lib/elasticsearch/persistence/version.rb +1 -1
  23. data/test/integration/repository/custom_class_test.rb +85 -0
  24. data/test/integration/repository/customized_class_test.rb +82 -0
  25. data/test/integration/repository/default_class_test.rb +108 -0
  26. data/test/integration/repository/virtus_model_test.rb +114 -0
  27. data/test/test_helper.rb +46 -0
  28. data/test/unit/persistence_test.rb +32 -0
  29. data/test/unit/repository_class_test.rb +51 -0
  30. data/test/unit/repository_client_test.rb +32 -0
  31. data/test/unit/repository_find_test.rb +375 -0
  32. data/test/unit/repository_indexing_test.rb +37 -0
  33. data/test/unit/repository_module_test.rb +144 -0
  34. data/test/unit/repository_naming_test.rb +146 -0
  35. data/test/unit/repository_response_results_test.rb +98 -0
  36. data/test/unit/repository_search_test.rb +97 -0
  37. data/test/unit/repository_serialize_test.rb +57 -0
  38. data/test/unit/repository_store_test.rb +287 -0
  39. metadata +288 -20
@@ -0,0 +1,118 @@
1
+ ENV['RACK_ENV'] = 'test'
2
+
3
+ at_exit { Elasticsearch::Test::IntegrationTestCase.__run_at_exit_hooks } if ENV['SERVER']
4
+
5
+ require 'test/unit'
6
+ require 'shoulda-context'
7
+ require 'mocha/setup'
8
+ require 'rack/test'
9
+ require 'turn'
10
+
11
+ require 'elasticsearch/extensions/test/cluster'
12
+ require 'elasticsearch/extensions/test/startup_shutdown'
13
+
14
+ require_relative 'application'
15
+
16
+ NoteRepository.index_name = 'notes_test'
17
+
18
+ class Elasticsearch::Persistence::ExampleApplicationTest < Test::Unit::TestCase
19
+ include Rack::Test::Methods
20
+ alias :response :last_response
21
+
22
+ def app
23
+ Application.new
24
+ end
25
+
26
+ context "Note" do
27
+ should "be initialized with a Hash" do
28
+ note = Note.new 'foo' => 'bar'
29
+ assert_equal 'bar', note.attributes['foo']
30
+ end
31
+
32
+ should "add created_at when it's not passed" do
33
+ note = Note.new
34
+ assert_not_nil note.created_at
35
+ assert_match /#{Time.now.year}/, note.created_at
36
+ end
37
+
38
+ should "not add created_at when it's passed" do
39
+ note = Note.new 'created_at' => 'FOO'
40
+ assert_equal 'FOO', note.created_at
41
+ end
42
+
43
+ should "trim long text" do
44
+ assert_equal 'Hello World', Note.new('text' => 'Hello World').text
45
+ assert_equal 'FOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFO (...)',
46
+ Note.new('text' => 'FOO'*200).text
47
+ end
48
+
49
+ should "delegate methods to attributes" do
50
+ note = Note.new 'foo' => 'bar'
51
+ assert_equal 'bar', note.foo
52
+ end
53
+
54
+ should "have tags" do
55
+ assert_not_nil Note.new.tags
56
+ end
57
+
58
+ should "provide a `to_hash` method" do
59
+ note = Note.new 'foo' => 'bar'
60
+ assert_instance_of Hash, note.to_hash
61
+ assert_equal ['created_at', 'foo'], note.to_hash.keys.sort
62
+ end
63
+
64
+ should "extract tags from the text" do
65
+ note = Note.new 'text' => 'Hello [foo] [bar]'
66
+ assert_equal 'Hello', note.text
67
+ assert_equal ['foo', 'bar'], note.tags
68
+ end
69
+ end
70
+
71
+ context "Application" do
72
+ setup do
73
+ app.settings.repository.client = Elasticsearch::Client.new \
74
+ hosts: [{ host: 'localhost', port: ENV.fetch('TEST_CLUSTER_PORT', 9250)}],
75
+ log: true
76
+ app.settings.repository.client.transport.logger.formatter = proc { |s, d, p, m| "\e[2m#{m}\n\e[0m" }
77
+ app.settings.repository.create_index! force: true
78
+ app.settings.repository.client.cluster.health wait_for_status: 'yellow'
79
+ end
80
+
81
+ should "have the correct index name" do
82
+ assert_equal 'notes_test', app.settings.repository.index
83
+ end
84
+
85
+ should "display empty page when there are no notes" do
86
+ get '/'
87
+ assert response.ok?, response.status.to_s
88
+ assert_match /No notes found/, response.body.to_s
89
+ end
90
+
91
+ should "display the notes" do
92
+ app.settings.repository.save Note.new('text' => 'Hello')
93
+ app.settings.repository.refresh_index!
94
+
95
+ get '/'
96
+ assert response.ok?, response.status.to_s
97
+ assert_match /<p>\s*Hello/, response.body.to_s
98
+ end
99
+
100
+ should "create a note" do
101
+ post '/', { 'text' => 'Hello World' }
102
+ follow_redirect!
103
+
104
+ assert response.ok?, response.status.to_s
105
+ assert_match /Hello World/, response.body.to_s
106
+ end
107
+
108
+ should "delete a note" do
109
+ app.settings.repository.save Note.new('id' => 'foobar', 'text' => 'Perish...')
110
+ delete "/foobar"
111
+ follow_redirect!
112
+
113
+ assert response.ok?, response.status.to_s
114
+ assert_no_match /Perish/, response.body.to_s
115
+ end
116
+ end
117
+
118
+ end
@@ -1,7 +1,93 @@
1
- require "elasticsearch/persistence/version"
1
+ require 'elasticsearch'
2
+ require 'elasticsearch/model/indexing'
3
+ require 'hashie'
4
+
5
+ require 'active_support/inflector'
6
+
7
+ require 'elasticsearch/persistence/version'
8
+
9
+ require 'elasticsearch/persistence/client'
10
+ require 'elasticsearch/persistence/repository/response/results'
11
+ require 'elasticsearch/persistence/repository/naming'
12
+ require 'elasticsearch/persistence/repository/serialize'
13
+ require 'elasticsearch/persistence/repository/store'
14
+ require 'elasticsearch/persistence/repository/find'
15
+ require 'elasticsearch/persistence/repository/search'
16
+ require 'elasticsearch/persistence/repository/class'
17
+ require 'elasticsearch/persistence/repository'
2
18
 
3
19
  module Elasticsearch
20
+
21
+ # Persistence for Ruby domain objects and models in Elasticsearch
22
+ # ===============================================================
23
+ #
24
+ # `Elasticsearch::Persistence` contains modules for storing and retrieving Ruby domain objects and models
25
+ # in Elasticsearch.
26
+ #
27
+ # == Repository
28
+ #
29
+ # The repository patterns allows to store and retrieve Ruby objects in Elasticsearch.
30
+ #
31
+ # require 'elasticsearch/persistence'
32
+ #
33
+ # class Note
34
+ # def to_hash; {foo: 'bar'}; end
35
+ # end
36
+ #
37
+ # repository = Elasticsearch::Persistence::Repository.new
38
+ #
39
+ # repository.save Note.new
40
+ # # => {"_index"=>"repository", "_type"=>"note", "_id"=>"mY108X9mSHajxIy2rzH2CA", ...}
41
+ #
42
+ # Customize your repository by including the main module in a Ruby class
43
+ # class MyRepository
44
+ # include Elasticsearch::Persistence::Repository
45
+ #
46
+ # index 'my_notes'
47
+ # klass Note
48
+ #
49
+ # client Elasticsearch::Client.new log: true
50
+ # end
51
+ #
52
+ # repository = MyRepository.new
53
+ #
54
+ # repository.save Note.new
55
+ # # 2014-04-04 22:15:25 +0200: POST http://localhost:9200/my_notes/note [status:201, request:0.009s, query:n/a]
56
+ # # 2014-04-04 22:15:25 +0200: > {"foo":"bar"}
57
+ # # 2014-04-04 22:15:25 +0200: < {"_index":"my_notes","_type":"note","_id":"-d28yXLFSlusnTxb13WIZQ", ...}
58
+ #
4
59
  module Persistence
5
- # Your code goes here...
60
+
61
+ # :nodoc:
62
+ module ClassMethods
63
+
64
+ # Get or set the default client for all repositories and models
65
+ #
66
+ # @example Set and configure the default client
67
+ #
68
+ # Elasticsearch::Persistence.client Elasticsearch::Client.new host: 'http://localhost:9200', tracer: true
69
+ #
70
+ # @example Perform an API request through the client
71
+ #
72
+ # Elasticsearch::Persistence.client.cluster.health
73
+ # # => { "cluster_name" => "elasticsearch" ... }
74
+ #
75
+ def client client=nil
76
+ @client = client || @client || Elasticsearch::Client.new
77
+ end
78
+
79
+ # Set the default client for all repositories and models
80
+ #
81
+ # @example Set and configure the default client
82
+ #
83
+ # Elasticsearch::Persistence.client = Elasticsearch::Client.new host: 'http://localhost:9200', tracer: true
84
+ # => #<Elasticsearch::Transport::Client:0x007f96a6dd0d80 @transport=... >
85
+ #
86
+ def client=(client)
87
+ @client = client
88
+ end
89
+ end
90
+
91
+ extend ClassMethods
6
92
  end
7
93
  end
@@ -0,0 +1,51 @@
1
+ module Elasticsearch
2
+ module Persistence
3
+ module Repository
4
+
5
+ # Wraps the Elasticsearch Ruby
6
+ # [client](https://github.com/elasticsearch/elasticsearch-ruby/tree/master/elasticsearch#usage)
7
+ #
8
+ module Client
9
+
10
+ # Get or set the default client for this repository
11
+ #
12
+ # @example Set and configure the client for the repository class
13
+ #
14
+ # class MyRepository
15
+ # include Elasticsearch::Persistence::Repository
16
+ # client Elasticsearch::Client.new host: 'http://localhost:9200', log: true
17
+ # end
18
+ #
19
+ # @example Set and configure the client for this repository instance
20
+ #
21
+ # repository.client Elasticsearch::Client.new host: 'http://localhost:9200', tracer: true
22
+ #
23
+ # @example Perform an API request through the client
24
+ #
25
+ # MyRepository.client.cluster.health
26
+ # repository.client.cluster.health
27
+ # # => { "cluster_name" => "elasticsearch" ... }
28
+ #
29
+ def client client=nil
30
+ @client = client || @client || Elasticsearch::Persistence.client
31
+ end
32
+
33
+ # Set the default client for this repository
34
+ #
35
+ # @example Set and configure the client for the repository class
36
+ #
37
+ # MyRepository.client = Elasticsearch::Client.new host: 'http://localhost:9200', log: true
38
+ #
39
+ # @example Set and configure the client for this repository instance
40
+ #
41
+ # repository.client = Elasticsearch::Client.new host: 'http://localhost:9200', tracer: true
42
+ #
43
+ def client=(client)
44
+ @client = client
45
+ @client
46
+ end
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,75 @@
1
+ module Elasticsearch
2
+ module Persistence
3
+
4
+ # Delegate methods to the repository (acting as a gateway)
5
+ #
6
+ module GatewayDelegation
7
+ def method_missing(method_name, *arguments, &block)
8
+ gateway.respond_to?(method_name) ? gateway.__send__(method_name, *arguments, &block) : super
9
+ end
10
+
11
+ def respond_to?(method_name, include_private=false)
12
+ gateway.respond_to?(method_name) || super
13
+ end
14
+
15
+ def respond_to_missing?(method_name, *)
16
+ gateway.respond_to?(method_name) || super
17
+ end
18
+ end
19
+
20
+ # When included, creates an instance of the {Repository::Class} class as a "gateway"
21
+ #
22
+ # @example Include the repository in a custom class
23
+ #
24
+ # class MyRepository
25
+ # include Elasticsearch::Persistence::Repository
26
+ # end
27
+ #
28
+ module Repository
29
+ def self.included(base)
30
+ gateway = Elasticsearch::Persistence::Repository::Class.new host: base
31
+
32
+ # Define the instance level gateway
33
+ #
34
+ base.class_eval do
35
+ define_method :gateway do
36
+ @gateway ||= gateway
37
+ end
38
+
39
+ include GatewayDelegation
40
+ end
41
+
42
+ # Define the class level gateway
43
+ #
44
+ (class << base; self; end).class_eval do
45
+ define_method :gateway do |&block|
46
+ @gateway ||= gateway
47
+ @gateway.instance_eval(&block) if block
48
+ @gateway
49
+ end
50
+
51
+ include GatewayDelegation
52
+ end
53
+
54
+ # Catch repository methods (such as `serialize` and others) defined in the receiving class,
55
+ # and overload the default definition in the gateway
56
+ #
57
+ def base.method_added(name)
58
+ if :gateway != name && respond_to?(:gateway) && (gateway.public_methods - Object.public_methods).include?(name)
59
+ gateway.define_singleton_method(name, self.new.method(name).to_proc)
60
+ end
61
+ end
62
+ end
63
+
64
+ # Shortcut method to allow concise repository initialization
65
+ #
66
+ # @example Create a new default repository
67
+ #
68
+ # repository = Elasticsearch::Persistence::Repository.new
69
+ #
70
+ def new(options={}, &block)
71
+ Elasticsearch::Persistence::Repository::Class.new( {index: 'repository'}.merge(options), &block )
72
+ end; module_function :new
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,71 @@
1
+ module Elasticsearch
2
+ module Persistence
3
+ module Repository
4
+
5
+ # The default repository class, to be used either directly, or as a gateway in a custom repository class
6
+ #
7
+ # @example Standalone use
8
+ #
9
+ # repository = Elasticsearch::Persistence::Repository::Class.new
10
+ # # => #<Elasticsearch::Persistence::Repository::Class ...>
11
+ # repository.save(my_object)
12
+ # # => {"_index"=> ... }
13
+ #
14
+ # @example Shortcut use
15
+ #
16
+ # repository = Elasticsearch::Persistence::Repository.new
17
+ # # => #<Elasticsearch::Persistence::Repository::Class ...>
18
+ #
19
+ # @example Configuration via a block
20
+ #
21
+ # repository = Elasticsearch::Persistence::Repository.new do
22
+ # index 'my_notes'
23
+ # end
24
+ #
25
+ # # => #<Elasticsearch::Persistence::Repository::Class ...>
26
+ # # > repository.save(my_object)
27
+ # # => {"_index"=> ... }
28
+ #
29
+ # @example Accessing the gateway in a custom class
30
+ #
31
+ # class MyRepository
32
+ # include Elasticsearch::Persistence::Repository
33
+ # end
34
+ #
35
+ # repository = MyRepository.new
36
+ #
37
+ # repository.gateway.client.info
38
+ # # => {"status"=>200, "name"=>"Venom", ... }
39
+ #
40
+ class Class
41
+ include Elasticsearch::Persistence::Repository::Client
42
+ include Elasticsearch::Persistence::Repository::Naming
43
+ include Elasticsearch::Persistence::Repository::Serialize
44
+ include Elasticsearch::Persistence::Repository::Store
45
+ include Elasticsearch::Persistence::Repository::Find
46
+ include Elasticsearch::Persistence::Repository::Search
47
+
48
+ include Elasticsearch::Model::Indexing::ClassMethods
49
+
50
+ attr_reader :options
51
+
52
+ def initialize(options={}, &block)
53
+ @options = options
54
+ index_name options.delete(:index)
55
+ block.arity < 1 ? instance_eval(&block) : block.call(self) if block_given?
56
+ end
57
+
58
+ # Return the "host" class, if this repository is a gateway hosted in another class
59
+ #
60
+ # @return [nil, Class]
61
+ #
62
+ # @api private
63
+ #
64
+ def host
65
+ options[:host]
66
+ end
67
+ end
68
+
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,73 @@
1
+ module Elasticsearch
2
+ module Persistence
3
+ module Repository
4
+ class DocumentNotFound < StandardError; end
5
+
6
+ # Retrieves one or more domain objects from the repository
7
+ #
8
+ module Find
9
+
10
+ # Retrieve a single object or multiple objects from Elasticsearch by ID or IDs
11
+ #
12
+ # @example Retrieve a single object by ID
13
+ #
14
+ # repository.find(1)
15
+ # # => <Note ...>
16
+ #
17
+ # @example Retrieve multiple objects by IDs
18
+ #
19
+ # repository.find(1, 2)
20
+ # # => [<Note ...>, <Note ...>
21
+ #
22
+ # @return [Object,Array]
23
+ #
24
+ def find(*args)
25
+ options = args.last.is_a?(Hash) ? args.pop : {}
26
+ ids = args
27
+
28
+ if args.size == 1
29
+ id = args.pop
30
+ id.is_a?(Array) ? __find_many(id, options) : __find_one(id, options)
31
+ else
32
+ __find_many args, options
33
+ end
34
+ end
35
+
36
+ # Return if object exists in the repository
37
+ #
38
+ # @example
39
+ #
40
+ # repository.exists?(1)
41
+ # => true
42
+ #
43
+ # @return [true, false]
44
+ #
45
+ def exists?(id, options={})
46
+ type = document_type || (klass ? __get_type_from_class(klass) : '_all')
47
+ client.exists( { index: index_name, type: type, id: id }.merge(options) )
48
+ end
49
+
50
+ # @api private
51
+ #
52
+ def __find_one(id, options={})
53
+ type = document_type || (klass ? __get_type_from_class(klass) : '_all')
54
+ document = client.get( { index: index_name, type: type, id: id }.merge(options) )
55
+
56
+ deserialize(document)
57
+ rescue Elasticsearch::Transport::Transport::Errors::NotFound => e
58
+ raise DocumentNotFound, e.message, caller
59
+ end
60
+
61
+ # @api private
62
+ #
63
+ def __find_many(ids, options={})
64
+ type = document_type || (klass ? __get_type_from_class(klass) : '_all')
65
+ documents = client.mget( { index: index_name, type: type, body: { ids: ids } }.merge(options) )
66
+
67
+ documents['docs'].map { |document| document['found'] ? deserialize(document) : nil }
68
+ end
69
+ end
70
+
71
+ end
72
+ end
73
+ end