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.
- checksums.yaml +15 -0
- data/LICENSE.txt +10 -19
- data/README.md +432 -14
- data/Rakefile +56 -0
- data/elasticsearch-persistence.gemspec +45 -17
- data/examples/sinatra/.gitignore +7 -0
- data/examples/sinatra/Gemfile +28 -0
- data/examples/sinatra/README.markdown +36 -0
- data/examples/sinatra/application.rb +238 -0
- data/examples/sinatra/config.ru +7 -0
- data/examples/sinatra/test.rb +118 -0
- data/lib/elasticsearch/persistence.rb +88 -2
- data/lib/elasticsearch/persistence/client.rb +51 -0
- data/lib/elasticsearch/persistence/repository.rb +75 -0
- data/lib/elasticsearch/persistence/repository/class.rb +71 -0
- data/lib/elasticsearch/persistence/repository/find.rb +73 -0
- data/lib/elasticsearch/persistence/repository/naming.rb +115 -0
- data/lib/elasticsearch/persistence/repository/response/results.rb +90 -0
- data/lib/elasticsearch/persistence/repository/search.rb +60 -0
- data/lib/elasticsearch/persistence/repository/serialize.rb +31 -0
- data/lib/elasticsearch/persistence/repository/store.rb +95 -0
- data/lib/elasticsearch/persistence/version.rb +1 -1
- data/test/integration/repository/custom_class_test.rb +85 -0
- data/test/integration/repository/customized_class_test.rb +82 -0
- data/test/integration/repository/default_class_test.rb +108 -0
- data/test/integration/repository/virtus_model_test.rb +114 -0
- data/test/test_helper.rb +46 -0
- data/test/unit/persistence_test.rb +32 -0
- data/test/unit/repository_class_test.rb +51 -0
- data/test/unit/repository_client_test.rb +32 -0
- data/test/unit/repository_find_test.rb +375 -0
- data/test/unit/repository_indexing_test.rb +37 -0
- data/test/unit/repository_module_test.rb +144 -0
- data/test/unit/repository_naming_test.rb +146 -0
- data/test/unit/repository_response_results_test.rb +98 -0
- data/test/unit/repository_search_test.rb +97 -0
- data/test/unit/repository_serialize_test.rb +57 -0
- data/test/unit/repository_store_test.rb +287 -0
- 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
|
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
|
-
|
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
|