oedipus-dm 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.
- data/.gitignore +10 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +451 -0
- data/Rakefile +17 -0
- data/lib/oedipus/data_mapper/collection.rb +52 -0
- data/lib/oedipus/data_mapper/conversions.rb +64 -0
- data/lib/oedipus/data_mapper/index.rb +268 -0
- data/lib/oedipus/data_mapper/pagination.rb +48 -0
- data/lib/oedipus/data_mapper/version.rb +14 -0
- data/lib/oedipus/data_mapper.rb +44 -0
- data/lib/oedipus-dm.rb +10 -0
- data/oedipus-dm.gemspec +31 -0
- data/spec/data/.gitkeep +0 -0
- data/spec/integration/index_spec.rb +398 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/models/post.rb +11 -0
- data/spec/support/models/user.rb +8 -0
- metadata +134 -0
@@ -0,0 +1,268 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
##
|
4
|
+
# DataMapper Integration for Oedipus.
|
5
|
+
# Copyright © 2012 Chris Corbyn.
|
6
|
+
#
|
7
|
+
# See LICENSE file for details.
|
8
|
+
##
|
9
|
+
|
10
|
+
module Oedipus
|
11
|
+
module DataMapper
|
12
|
+
# Provides a gateway between a DataMapper model and Oedipus.
|
13
|
+
class Index
|
14
|
+
include Conversions
|
15
|
+
include Pagination
|
16
|
+
|
17
|
+
attr_reader :model
|
18
|
+
attr_reader :name
|
19
|
+
attr_reader :connection
|
20
|
+
|
21
|
+
# Initialize a new Index for the given model.
|
22
|
+
#
|
23
|
+
# @param [DataMapper::Model] model
|
24
|
+
# the model stored in the sphinx index
|
25
|
+
#
|
26
|
+
# @param [Hash] options
|
27
|
+
# additonal configuration options
|
28
|
+
#
|
29
|
+
# @option [Symbol] name
|
30
|
+
# the name of the sphinx index, optional
|
31
|
+
# (defaults to the model storage_name)
|
32
|
+
#
|
33
|
+
# @option [Connection] connection
|
34
|
+
# an instance of an Oedipus::Connection
|
35
|
+
# (defaults to the globally configured connection)
|
36
|
+
#
|
37
|
+
# @yields [Index] self
|
38
|
+
# the index, so that mappings can be configured
|
39
|
+
def initialize(model, options = {})
|
40
|
+
@model = model
|
41
|
+
@name = options[:name] || model.storage_name
|
42
|
+
@connection = options[:connection] || Oedipus::DataMapper.connection
|
43
|
+
@mappings = {}
|
44
|
+
@key = model.key.first.name
|
45
|
+
|
46
|
+
map(:id, with: @key)
|
47
|
+
|
48
|
+
yield self
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns the underlying Index, for carrying out low-level operations.
|
52
|
+
#
|
53
|
+
# @return [Oedipus::Index]
|
54
|
+
# the underlying Index, used by Oedipus
|
55
|
+
def raw
|
56
|
+
@raw ||= connection[name]
|
57
|
+
end
|
58
|
+
|
59
|
+
# Insert the given resource into a realtime index.
|
60
|
+
#
|
61
|
+
# Fields and attributes will be read from any configured mappings.
|
62
|
+
#
|
63
|
+
# @param [DataMapper::Resource] resource
|
64
|
+
# an instance of the model this index manages
|
65
|
+
#
|
66
|
+
# @return [Fixnum]
|
67
|
+
# the number of resources inserted (currently always 1)
|
68
|
+
def insert(resource)
|
69
|
+
record = @mappings.inject({}) do |r, (k, mapping)|
|
70
|
+
r.merge!(k => mapping[:get].call(resource))
|
71
|
+
end
|
72
|
+
|
73
|
+
unless id = record.delete(:id)
|
74
|
+
raise ArgumentError, "Attempted to insert a record without an ID"
|
75
|
+
end
|
76
|
+
|
77
|
+
raw.insert(id, record)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Update the given resource in a realtime index.
|
81
|
+
#
|
82
|
+
# Fields and attributes will be read from any configured mappings.
|
83
|
+
#
|
84
|
+
# @param [DataMapper::Resource] resource
|
85
|
+
# an instance of the model this index manages
|
86
|
+
#
|
87
|
+
# @return [Fixnum]
|
88
|
+
# the number of resources updated (currently always 1 or 0)
|
89
|
+
def update(resource)
|
90
|
+
record = @mappings.inject({}) do |r, (k, mapping)|
|
91
|
+
r.merge!(k => mapping[:get].call(resource))
|
92
|
+
end
|
93
|
+
|
94
|
+
unless id = record.delete(:id)
|
95
|
+
raise ArgumentError, "Attempted to update a record without an ID"
|
96
|
+
end
|
97
|
+
|
98
|
+
raw.update(id, record)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Delete the given resource from a realtime index.
|
102
|
+
#
|
103
|
+
# @param [DataMapper::Resource] resource
|
104
|
+
# an instance of the model this index manages
|
105
|
+
#
|
106
|
+
# @return [Fixnum]
|
107
|
+
# the number of resources updated (currently always 1 or 0)
|
108
|
+
def delete(resource)
|
109
|
+
unless id = @mappings[:id][:get].call(resource)
|
110
|
+
raise ArgumentError, "Attempted to delete a record without an ID"
|
111
|
+
end
|
112
|
+
|
113
|
+
raw.delete(id)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Fully replace the given resource in a realtime index.
|
117
|
+
#
|
118
|
+
# Fields and attributes will be read from any configured mappings.
|
119
|
+
#
|
120
|
+
# @param [DataMapper::Resource] resource
|
121
|
+
# an instance of the model this index manages
|
122
|
+
#
|
123
|
+
# @return [Fixnum]
|
124
|
+
# the number of resources replaced (currently always 1)
|
125
|
+
def replace(resource)
|
126
|
+
record = @mappings.inject({}) do |r, (k, mapping)|
|
127
|
+
r.merge!(k => mapping[:get].call(resource))
|
128
|
+
end
|
129
|
+
|
130
|
+
unless id = record.delete(:id)
|
131
|
+
raise ArgumentError, "Attempted to replace a record without an ID"
|
132
|
+
end
|
133
|
+
|
134
|
+
raw.replace(id, record)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Perform a fulltext and/or attribute search.
|
138
|
+
#
|
139
|
+
# This method searches in the sphinx index, using Oedipus then returns
|
140
|
+
# the corresponding collection of DataMapper records.
|
141
|
+
#
|
142
|
+
# No query is issued directly to the DataMapper repository, though only
|
143
|
+
# the handled attributes will be loaded, meaning lazy-loading will occur
|
144
|
+
# should any other attributes be accessed.
|
145
|
+
#
|
146
|
+
# A faceted search may be performed by passing in the :facets option. All
|
147
|
+
# facets are returned via a #facets accessor on the collection.
|
148
|
+
#
|
149
|
+
# @param [String] fulltext_query
|
150
|
+
# a fulltext query to send to sphinx, optional
|
151
|
+
#
|
152
|
+
# @param [Hash] options
|
153
|
+
# options for filtering, facets, sorting and range-limiting
|
154
|
+
#
|
155
|
+
# @return [Collecton]
|
156
|
+
# a collection object containing the given resources
|
157
|
+
#
|
158
|
+
# @option [Array] attrs
|
159
|
+
# a list of attributes to fetch (supports '*' and complex expressions)
|
160
|
+
#
|
161
|
+
# @option [Hash] facets
|
162
|
+
# a map of facets to execute, based on the base query (see the main
|
163
|
+
# oedipus gem for full details)
|
164
|
+
#
|
165
|
+
# @option [Fixnum] limit
|
166
|
+
# a limit to apply
|
167
|
+
#
|
168
|
+
# @option [Fixnum] offset
|
169
|
+
# an offset to search from
|
170
|
+
#
|
171
|
+
# @option [Hash] order
|
172
|
+
# a map of attribute names with either :asc or :desc
|
173
|
+
#
|
174
|
+
# @option [Object] *
|
175
|
+
# all other options are taken to be attribute filters
|
176
|
+
def search(*args)
|
177
|
+
filters = convert_filters(args)
|
178
|
+
pager_options = extract_pager_options(filters)
|
179
|
+
build_collection(raw.search(*filters).merge(pager_options: pager_options))
|
180
|
+
end
|
181
|
+
|
182
|
+
# Perform multiple unrelated searches on the index.
|
183
|
+
#
|
184
|
+
# Accepts a Hash of varying searches and returns a Hash of results.
|
185
|
+
#
|
186
|
+
# @param [Hash] searches
|
187
|
+
# a Hash, whose keys are named searches and whose values are arguments
|
188
|
+
# to #search
|
189
|
+
#
|
190
|
+
# @return [Hash]
|
191
|
+
# a Hash whose keys are the same as the inputs and whose values are
|
192
|
+
# the corresponding results
|
193
|
+
def multi_search(searches)
|
194
|
+
raise ArgumentError, "Argument 1 for #multi_search must be a Hash" unless Hash === searches
|
195
|
+
|
196
|
+
raw.multi_search(
|
197
|
+
searches.inject({}) { |o, (k, v)|
|
198
|
+
o.merge!(k => convert_filters(v))
|
199
|
+
}
|
200
|
+
).inject({}) { |o, (k, v)|
|
201
|
+
o.merge!(k => build_collection(v))
|
202
|
+
}
|
203
|
+
end
|
204
|
+
|
205
|
+
# Map an attribute in the index with a property on the model.
|
206
|
+
#
|
207
|
+
# @param [Symbol] attr
|
208
|
+
# the attribute in the sphinx index
|
209
|
+
#
|
210
|
+
# @param [Hash] options
|
211
|
+
# mapping options
|
212
|
+
#
|
213
|
+
# @option [Symbol] with
|
214
|
+
# the property if the name is not the same as the sphinx attribute
|
215
|
+
#
|
216
|
+
# @option [Proc] set
|
217
|
+
# a proc/lambda that accepts a new resource, and the value,
|
218
|
+
# to set the value onto the resource
|
219
|
+
#
|
220
|
+
# @option [Proc] get
|
221
|
+
# a proc/lambda that accepts a resource and returns the value to set,
|
222
|
+
# for realtime indexes only
|
223
|
+
def map(attr, options = {})
|
224
|
+
@mappings[attr] = normalize_mapping(attr, options.dup)
|
225
|
+
end
|
226
|
+
|
227
|
+
private
|
228
|
+
|
229
|
+
def normalize_mapping(attr, options)
|
230
|
+
options.tap do
|
231
|
+
prop = options.delete(:with) || attr
|
232
|
+
options[:get] ||= ->(r) { r.send("#{prop}") }
|
233
|
+
options[:set] ||= ->(r, v) { r.send("#{prop}=", v) }
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def build_collection(result)
|
238
|
+
resources = result[:records].collect do |record|
|
239
|
+
record.inject(model.new) { |r, (k, v)|
|
240
|
+
r.tap { @mappings[k][:set].call(r, v) if @mappings.key?(k) }
|
241
|
+
}.tap { |r|
|
242
|
+
r.persistence_state = ::DataMapper::Resource::PersistenceState::Clean.new(r)
|
243
|
+
}
|
244
|
+
end
|
245
|
+
|
246
|
+
query = ::DataMapper::Query.new(
|
247
|
+
model.repository,
|
248
|
+
model,
|
249
|
+
fields: model.properties.select {|p| p.loaded?(resources.first)},
|
250
|
+
conditions: { @key => resources.map {|r| r[@key]} },
|
251
|
+
reload: false
|
252
|
+
)
|
253
|
+
|
254
|
+
Collection.new(
|
255
|
+
query,
|
256
|
+
resources,
|
257
|
+
time: result[:time],
|
258
|
+
total_found: result[:total_found],
|
259
|
+
count: result[:records].count,
|
260
|
+
keywords: result[:keywords],
|
261
|
+
docs: result[:docs],
|
262
|
+
facets: result.fetch(:facets, {}).inject({}) {|f, (k, v)| f.merge!(k => build_collection(v))},
|
263
|
+
pager: build_pager(result, result[:pager_options])
|
264
|
+
)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
##
|
4
|
+
# DataMapper Integration for Oedipus.
|
5
|
+
# Copyright © 2012 Chris Corbyn.
|
6
|
+
#
|
7
|
+
# See LICENSE file for details.
|
8
|
+
##
|
9
|
+
|
10
|
+
module Oedipus
|
11
|
+
module DataMapper
|
12
|
+
module Pagination
|
13
|
+
def pageable?
|
14
|
+
defined? ::DataMapper::Pagination
|
15
|
+
end
|
16
|
+
|
17
|
+
def extract_pager_options(filters)
|
18
|
+
return unless pageable?
|
19
|
+
|
20
|
+
options = filters.last
|
21
|
+
pager = options.delete(:pager)
|
22
|
+
|
23
|
+
unless pager.nil?
|
24
|
+
return unless page = pager[:page]
|
25
|
+
|
26
|
+
page = [page.to_i, 1].max
|
27
|
+
per_page = pager.fetch(:per_page, ::DataMapper::Pagination.defaults[:per_page]).to_i
|
28
|
+
page_param = pager.fetch(:page_param, ::DataMapper::Pagination.defaults[:page_param])
|
29
|
+
|
30
|
+
options[:limit] ||= per_page
|
31
|
+
options[:offset] ||= (page - 1) * per_page
|
32
|
+
|
33
|
+
pager.merge(
|
34
|
+
page_param => page,
|
35
|
+
:page_param => page_param,
|
36
|
+
:limit => options[:limit].to_i,
|
37
|
+
:offset => options[:offset].to_i
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def build_pager(results, options)
|
43
|
+
return unless pageable? && Hash === options
|
44
|
+
::DataMapper::Pager.new(options.merge(total: results[:total_found]))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
##
|
4
|
+
# DataMapper Integration for Oedipus.
|
5
|
+
# Copyright © 2012 Chris Corbyn.
|
6
|
+
#
|
7
|
+
# See LICENSE file for details.
|
8
|
+
##
|
9
|
+
|
10
|
+
require "oedipus"
|
11
|
+
require "dm-core"
|
12
|
+
|
13
|
+
require "oedipus/data_mapper/version"
|
14
|
+
require "oedipus/data_mapper/conversions"
|
15
|
+
require "oedipus/data_mapper/pagination"
|
16
|
+
require "oedipus/data_mapper/index"
|
17
|
+
require "oedipus/data_mapper/collection"
|
18
|
+
|
19
|
+
module Oedipus
|
20
|
+
module DataMapper
|
21
|
+
class << self
|
22
|
+
# Set up Oedipus::DataMapper with connection details.
|
23
|
+
#
|
24
|
+
# @yields [Struct<host, port>]
|
25
|
+
def configure
|
26
|
+
yield config
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns the configured connection.
|
30
|
+
#
|
31
|
+
# @return [Connection]
|
32
|
+
def connection
|
33
|
+
@connection ||= Connection.new(host: config.host, port: config.port)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns the configuration options.
|
37
|
+
#
|
38
|
+
# @return [Struct<host, port>]
|
39
|
+
def config
|
40
|
+
@config ||= Struct.new(:host, :port).new("localhost", 9306)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/oedipus-dm.rb
ADDED
data/oedipus-dm.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/oedipus/data_mapper/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["d11wtq"]
|
6
|
+
gem.email = ["chris@w3style.co.uk"]
|
7
|
+
gem.homepage = "https://github.com/d11wtq/oedipus-dm"
|
8
|
+
gem.summary = "DataMapper Integration for the Oedipus Sphinx 2 Client"
|
9
|
+
gem.description = <<-DESC.gsub(/^ {4}/m, "")
|
10
|
+
== DataMapper Integration for Oedipus
|
11
|
+
|
12
|
+
This gem adds the possibility to find DataMapper models by searching in
|
13
|
+
a Sphinx index, and to update/delete/replace them.
|
14
|
+
|
15
|
+
Faceted searches are cleanly supported.
|
16
|
+
DESC
|
17
|
+
|
18
|
+
gem.files = `git ls-files`.split($\)
|
19
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
20
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
21
|
+
gem.name = "oedipus-dm"
|
22
|
+
gem.require_paths = ["lib"]
|
23
|
+
gem.version = Oedipus::DataMapper::VERSION
|
24
|
+
|
25
|
+
gem.add_runtime_dependency "oedipus", ">= 0.0.5"
|
26
|
+
gem.add_runtime_dependency "dm-core", ">= 1.2"
|
27
|
+
|
28
|
+
gem.add_development_dependency "rake"
|
29
|
+
gem.add_development_dependency "rspec"
|
30
|
+
gem.add_development_dependency "dm-pager"
|
31
|
+
end
|
data/spec/data/.gitkeep
ADDED
File without changes
|