outoftime-sunspot 0.0.2 → 0.7.0

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.
@@ -1,43 +1,110 @@
1
1
  module Sunspot
2
+ #
3
+ # A Sunspot session encapsulates a connection to Solr and a set of
4
+ # configuration choices. Though users of Sunspot may manually instantiate
5
+ # Session objects, in the general case it's easier to use the singleton
6
+ # stored in the Sunspot module. Since the Sunspot module provides all of
7
+ # the instance methods of Session as class methods, they are not documented
8
+ # again here.
9
+ #
2
10
  class Session
3
11
  attr_reader :config
4
12
 
5
- def initialize(config = Sunspot::Configuration.build, connection = nil)
13
+ #
14
+ # Sessions are initialized with a Sunspot configuration and a Solr
15
+ # connection. Usually you will want to stick with the default arguments
16
+ # when instantiating your own sessions.
17
+ #
18
+ def initialize(config = Configuration.build, connection = nil)
6
19
  @config = config
7
20
  yield(@config) if block_given?
8
21
  @connection = connection
9
22
  end
10
23
 
24
+ #
25
+ # See Sunspot.search
26
+ #
11
27
  def search(*types, &block)
12
- ::Sunspot::Search.new(connection, @config, *types, &block).execute!
28
+ types.flatten!
29
+ Search.new(connection, @config, *types, &block).execute!
13
30
  end
14
31
 
32
+ #
33
+ # See Sunspot.index
34
+ #
15
35
  def index(*objects)
36
+ objects.flatten!
16
37
  for object in objects
17
- ::Sunspot::Indexer.add(connection, object)
38
+ setup_for(object).indexer(connection).add(object)
18
39
  end
19
40
  end
20
41
 
42
+ #
43
+ # See Sunspot.index!
44
+ #
45
+ def index!(*objects)
46
+ index(*objects)
47
+ commit
48
+ end
49
+
50
+ #
51
+ # See Sunspot.commit!
52
+ #
53
+ def commit
54
+ connection.commit
55
+ end
56
+
57
+ #
58
+ # See Sunspot.remove
59
+ #
21
60
  def remove(*objects)
61
+ objects.flatten!
22
62
  for object in objects
23
- ::Sunspot::Indexer.remove(connection, object)
63
+ setup_for(object).indexer(connection).remove(object)
24
64
  end
25
65
  end
26
66
 
67
+ #
68
+ # See Sunspot.remove_all
69
+ #
27
70
  def remove_all(*classes)
71
+ classes.flatten!
28
72
  if classes.empty?
29
- ::Sunspot::Indexer.remove_all(connection)
73
+ Indexer.remove_all(connection)
30
74
  else
31
- classes.each do |clazz|
32
- ::Sunspot::Indexer.remove_all(connection, clazz)
75
+ for clazz in classes
76
+ Setup.for(clazz).indexer(connection).remove_all
33
77
  end
34
78
  end
35
79
  end
36
80
 
37
81
  private
38
82
 
83
+ #
84
+ # Get the Setup object for the given object's class.
85
+ #
86
+ # ==== Parameters
87
+ #
88
+ # object<Object>:: The object whose setup is to be retrieved
89
+ #
90
+ # ==== Returns
91
+ #
92
+ # Sunspot::Setup:: The setup for the object's class
93
+ #
94
+ def setup_for(object)
95
+ Setup.for(object.class) || raise(ArgumentError, "Sunspot is not configured for #{object.class.inspect}")
96
+ end
97
+
98
+ #
99
+ # Retrieve the Solr connection for this session, creating one if it does not
100
+ # already exist.
101
+ #
102
+ # ==== Returns
103
+ #
104
+ # Solr::Connection:: The connection for this session
105
+ #
39
106
  def connection
40
- @connection ||= Solr::Connection.new(config.solr.url, :autocommit => :on)
107
+ @connection ||= Solr::Connection.new(config.solr.url)
41
108
  end
42
109
  end
43
110
  end
@@ -0,0 +1,171 @@
1
+ module Sunspot
2
+ #
3
+ # This class encapsulates the search/indexing setup for a given class. Its
4
+ # contents are built using the Sunspot.setup method.
5
+ #
6
+ class Setup #:nodoc:
7
+ def initialize(clazz)
8
+ @class_name = clazz.name
9
+ @fields, @text_fields = [], []
10
+ @dsl = DSL::Fields.new(self)
11
+ end
12
+
13
+ #
14
+ # Add fields for scope/ordering
15
+ #
16
+ # ==== Parameters
17
+ #
18
+ # fields<Array>:: Array of Sunspot::Field objects
19
+ #
20
+ def add_fields(fields)
21
+ @fields.concat(Array(fields))
22
+ end
23
+
24
+ #
25
+ # Add fields for fulltext search
26
+ #
27
+ # ==== Parameters
28
+ #
29
+ # fields<Array>:: Array of Sunspot::Field objects
30
+ #
31
+ def add_text_fields(fields)
32
+ @text_fields.concat(Array(fields))
33
+ end
34
+
35
+ #
36
+ # Builder method for evaluating the setup DSL
37
+ #
38
+ def setup(&block)
39
+ @dsl.instance_eval(&block)
40
+ end
41
+
42
+ #
43
+ # Get the fields associated with this setup as well as all inherited fields
44
+ #
45
+ # ==== Returns
46
+ #
47
+ # Array:: Collection of all fields associated with this setup
48
+ #
49
+ def fields
50
+ fields = @fields.dup
51
+ fields.concat(parent.fields) if parent
52
+ fields
53
+ end
54
+
55
+ #
56
+ # Get the text fields associated with this setup as well as all inherited
57
+ # text fields
58
+ #
59
+ # ==== Returns
60
+ #
61
+ # Array:: Collection of all text fields associated with this setup
62
+ #
63
+ def text_fields
64
+ text_fields = @text_fields.dup
65
+ text_fields.concat(parent.text_fields) if parent
66
+ text_fields
67
+ end
68
+
69
+ #
70
+ # Get all scope and text fields associated with this setup as well as all
71
+ # inherited fields
72
+ #
73
+ # ==== Returns
74
+ #
75
+ # Array:: Collection of all text and scope fields associated with this setup
76
+ #
77
+ def all_fields
78
+ fields + text_fields
79
+ end
80
+
81
+ #
82
+ # Factory method for an Indexer object configured to use this setup
83
+ #
84
+ # ==== Returns
85
+ #
86
+ # Sunspot::Indexer:: Indexer configured with this setup
87
+ def indexer(connection)
88
+ Indexer.new(connection, self)
89
+ end
90
+
91
+ #
92
+ # Return the class associated with this setup.
93
+ #
94
+ # ==== Returns
95
+ #
96
+ # clazz<Class>:: Class setup is configured for
97
+ #
98
+ def clazz
99
+ Util.full_const_get(@class_name)
100
+ end
101
+
102
+ protected
103
+
104
+ #
105
+ # Get the nearest inherited setup, if any
106
+ #
107
+ # ==== Returns
108
+ #
109
+ # Sunspot::Setup:: Setup for the nearest ancestor of this setup's class
110
+ #
111
+ def parent
112
+ Setup.for(clazz.superclass)
113
+ end
114
+
115
+ class <<self
116
+ #
117
+ # Retrieve or create the Setup instance for the given class, evaluating
118
+ # the given block to add to the setup's configuration
119
+ #
120
+ def setup(clazz, &block) #:nodoc:
121
+ self.for!(clazz).setup(&block)
122
+ end
123
+
124
+ #
125
+ # Retrieve the setup instance for the given class, or for the nearest
126
+ # ancestor that has a setup, if any.
127
+ #
128
+ # ==== Parameters
129
+ #
130
+ # clazz<Class>:: Class for which to retrieve a setup
131
+ #
132
+ # ==== Returns
133
+ #
134
+ # Sunspot::Setup::
135
+ # Setup instance associated with the given class or its nearest ancestor
136
+ #
137
+ def for(clazz) #:nodoc:
138
+ setups[clazz.name.to_sym] || self.for(clazz.superclass) if clazz
139
+ end
140
+
141
+ protected
142
+
143
+ #
144
+ # Retrieve or create a Setup instance for this class
145
+ #
146
+ # ==== Parameters
147
+ #
148
+ # clazz<Class>:: Class for which to retrieve a setup
149
+ #
150
+ # ==== Returns
151
+ #
152
+ # Sunspot::Setup:: New or existing setup for this class
153
+ #
154
+ def for!(clazz) #:nodoc:
155
+ setups[clazz.name.to_sym] ||= new(clazz)
156
+ end
157
+
158
+ private
159
+
160
+ # Singleton hash of class names to Setup instances
161
+ #
162
+ # ==== Returns
163
+ #
164
+ # Hash:: Class names keyed to Setup instances
165
+ #
166
+ def setups
167
+ @setups ||= {}
168
+ end
169
+ end
170
+ end
171
+ end
data/lib/sunspot/type.rb CHANGED
@@ -1,60 +1,144 @@
1
1
  module Sunspot
2
+ #
3
+ # This module contains singleton objects that represent the types that can be
4
+ # indexed and searched using Sunspot. Plugin developers should be able to
5
+ # add new constants to the Type module; as long as they implement the
6
+ # appropriate methods, Sunspot should be able to integrate them (note that
7
+ # this capability is untested at the moment). The required methods are:
8
+ #
9
+ # +indexed_name+::
10
+ # Convert a given field name into its form as stored in Solr. This
11
+ # generally means adding a suffix to match a Solr dynamicField definition.
12
+ # +to_indexed+::
13
+ # Convert a value of this type into the appropriate Solr string
14
+ # representation.
15
+ # +cast+::
16
+ # Convert a Solr string representation of a value into the appropriate
17
+ # Ruby type.
18
+ #
2
19
  module Type
3
- TextType = Module.new
4
- StringType = Module.new
5
- IntegerType = Module.new
6
- FloatType = Module.new
7
- TimeType = Module.new
8
-
9
- class <<TextType
10
- def indexed_name(name)
20
+ #
21
+ # Text is a special type that stores data for fulltext search. Unlike other
22
+ # types, Text fields are tokenized and are made available to the keyword
23
+ # search phrase. Text fields cannot be faceted, ordered upon, or used in
24
+ # restrictions. Similarly, text fields are the only fields that are made
25
+ # available to keyword search.
26
+ #
27
+ module TextType
28
+ class <<self
29
+ def indexed_name(name) #:nodoc:
11
30
  "#{name}_text"
12
- end
31
+ end
13
32
 
14
- def to_indexed(value)
15
- value.to_s if value
33
+ def to_indexed(value) #:nodoc:
34
+ value.to_s if value
35
+ end
16
36
  end
17
37
  end
18
38
 
19
- class <<StringType
20
- def indexed_name(name)
39
+ #
40
+ # The String type represents string data.
41
+ #
42
+ module StringType
43
+ class <<self
44
+ def indexed_name(name) #:nodoc:
21
45
  "#{name}_s"
22
- end
46
+ end
47
+
48
+ def to_indexed(value) #:nodoc:
49
+ value.to_s if value
50
+ end
23
51
 
24
- def to_indexed(value)
25
- value.to_s if value
52
+ def cast(string) #:nodoc:
53
+ string
54
+ end
26
55
  end
27
56
  end
28
57
 
29
- class <<IntegerType
30
- def indexed_name(name)
58
+ #
59
+ # The Integer type represents integers.
60
+ #
61
+ module IntegerType
62
+ class <<self
63
+ def indexed_name(name) #:nodoc:
31
64
  "#{name}_i"
32
- end
65
+ end
33
66
 
34
- def to_indexed(value)
35
- value.to_i.to_s if value
67
+ def to_indexed(value) #:nodoc:
68
+ value.to_i.to_s if value
69
+ end
70
+
71
+ def cast(string) #:nodoc:
72
+ string.to_i
73
+ end
36
74
  end
37
75
  end
38
76
 
39
- class <<FloatType
40
- def indexed_name(name)
77
+ #
78
+ # The Float type represents floating-point numbers.
79
+ #
80
+ module FloatType
81
+ class <<self
82
+ def indexed_name(name) #:nodoc:
41
83
  "#{name}_f"
42
- end
84
+ end
85
+
86
+ def to_indexed(value) #:nodoc:
87
+ value.to_f.to_s if value
88
+ end
43
89
 
44
- def to_indexed(value)
45
- value.to_f.to_s if value
90
+ def cast(string) #:nodoc:
91
+ string.to_f
92
+ end
46
93
  end
47
94
  end
48
95
 
49
- class <<TimeType
50
- def indexed_name(name)
96
+ #
97
+ # The time type represents times. Note that times are always converted to
98
+ # UTC before indexing, and facets of Time fields always return times in UTC.
99
+ #
100
+ module TimeType
101
+ class <<self
102
+ def indexed_name(name)
51
103
  "#{name}_d"
104
+ end
105
+
106
+ def to_indexed(value)
107
+ if value
108
+ time = value.respond_to?(:to_time) ? value.to_time : Time.parse(value.to_s)
109
+ time.utc.xmlschema
110
+ end
111
+ end
112
+
113
+ def cast(string)
114
+ Time.xmlschema(string)
115
+ end
52
116
  end
117
+ end
118
+
119
+ #
120
+ # The boolean type represents true/false values. Note that +nil+ will not be
121
+ # indexed at all; only +false+ will be indexed with a false value.
122
+ #
123
+ module BooleanType
124
+ class <<self
125
+ def indexed_name(name)
126
+ "#{name}_b"
127
+ end
128
+
129
+ def to_indexed(value)
130
+ unless value.nil?
131
+ value ? 'true' : 'false'
132
+ end
133
+ end
53
134
 
54
- def to_indexed(value)
55
- if value
56
- time = value.respond_to?(:to_time) ? value.to_time : Time.parse(value.to_s)
57
- time.utc.strftime('%FT%TZ')
135
+ def cast(string)
136
+ case string
137
+ when 'true'
138
+ true
139
+ when 'false'
140
+ false
141
+ end
58
142
  end
59
143
  end
60
144
  end
data/lib/sunspot/util.rb CHANGED
@@ -1,12 +1,146 @@
1
1
  module Sunspot
2
- module Util
3
- class ClosedStruct
4
- def initialize(data)
5
- (class <<self; self; end).module_eval do
6
- data.each_pair do |attr_name, value|
7
- define_method(attr_name.to_s) { value }
8
- end
2
+ #
3
+ # The Sunspot::Util module provides utility methods used elsewhere in the
4
+ # library.
5
+ #
6
+ module Util #:nodoc:
7
+ class <<self
8
+ #
9
+ # Get all of the superclasses for a given class, including the class
10
+ # itself.
11
+ #
12
+ # ==== Parameters
13
+ #
14
+ # clazz<Class>:: class for which to get superclasses
15
+ #
16
+ # ==== Returns
17
+ #
18
+ # Array:: Collection containing class and its superclasses
19
+ #
20
+ def superclasses_for(clazz)
21
+ superclasses = [clazz]
22
+ superclasses << (clazz = clazz.superclass) while clazz.superclass != Object
23
+ superclasses
24
+ end
25
+
26
+ #
27
+ # Convert a string to snake case
28
+ #
29
+ # ==== Parameters
30
+ #
31
+ # string<String>:: String to convert to snake case
32
+ #
33
+ # ==== Returns
34
+ #
35
+ # String:: String in snake case
36
+ #
37
+ def snake_case(string)
38
+ string.scan(/(^|[A-Z])([^A-Z]+)/).map! { |word| word.join.downcase }.join('_')
39
+ end
40
+
41
+ #
42
+ # Convert a string to camel case
43
+ #
44
+ # ==== Parameters
45
+ #
46
+ # string<String>:: String to convert to camel case
47
+ #
48
+ # ==== Returns
49
+ #
50
+ # String:: String in camel case
51
+ #
52
+ def camel_case(string)
53
+ string.split('_').map! { |word| word.capitalize }.join
54
+ end
55
+
56
+ #
57
+ # Get a constant from a fully qualified name
58
+ #
59
+ # ==== Parameters
60
+ #
61
+ # string<String>:: The fully qualified name of a constant
62
+ #
63
+ # ==== Returns
64
+ #
65
+ # Object:: Value of constant named
66
+ #
67
+ def full_const_get(string)
68
+ string.split('::').inject(Object) do |context, const_name|
69
+ context.const_get(const_name)
70
+ end
71
+ end
72
+
73
+ #
74
+ # Perform a deep merge of hashes, returning the result as a new hash.
75
+ # See #deep_merge_into for rules used to merge the hashes
76
+ #
77
+ # ==== Parameters
78
+ #
79
+ # left<Hash>:: Hash to merge
80
+ # right<Hash>:: The other hash to merge
81
+ #
82
+ # ==== Returns
83
+ #
84
+ # Hash:: New hash containing the given hashes deep-merged.
85
+ #
86
+ def deep_merge(left, right)
87
+ deep_merge_into({}, left, right)
88
+ end
89
+
90
+ #
91
+ # Perform a deep merge of the right hash into the left hash
92
+ #
93
+ # ==== Parameters
94
+ #
95
+ # left:: Hash to receive merge
96
+ # right:: Hash to merge into left
97
+ #
98
+ # ==== Returns
99
+ #
100
+ # Hash:: left
101
+ #
102
+ def deep_merge!(left, right)
103
+ deep_merge_into(left, left, right)
104
+ end
105
+
106
+ private
107
+
108
+ #
109
+ # Deep merge two hashes into a third hash, using rules that produce nice
110
+ # merged parameter hashes. The rules are as follows, for a given key:
111
+ #
112
+ # * If only one hash has a value, or if both hashes have the same value,
113
+ # just use the value.
114
+ # * If either of the values is not a hash, create arrays out of both
115
+ # values and concatenate them.
116
+ # * Otherwise, deep merge the two values (which are both hashes)
117
+ #
118
+ # ==== Parameters
119
+ #
120
+ # destination<Hash>:: Hash into which to perform the merge
121
+ # left<Hash>:: One hash to merge
122
+ # right<Hash>:: The other hash to merge
123
+ #
124
+ # ==== Returns
125
+ #
126
+ # Hash:: destination
127
+ #
128
+ def deep_merge_into(destination, left, right)
129
+ left.each_pair do |name, left_value|
130
+ right_value = right[name]
131
+ destination[name] =
132
+ if right_value.nil? || left_value == right_value
133
+ left_value
134
+ elsif !left_value.respond_to?(:each_pair) || !right_value.respond_to?(:each_pair)
135
+ Array(left_value) + Array(right_value)
136
+ else
137
+ merged_value = {}
138
+ deep_merge_into(merged_value, left_value, right_value)
139
+ end
9
140
  end
141
+ left_keys = Set.new(left.keys)
142
+ destination.merge!(right.reject { |k, v| left_keys.include?(k) })
143
+ destination
10
144
  end
11
145
  end
12
146
  end