elasticsearch-persistence 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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