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.
@@ -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,14 @@
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
+ VERSION = "0.0.1"
13
+ end
14
+ 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
@@ -0,0 +1,10 @@
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/data_mapper"
@@ -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
File without changes