endeca 1.3.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,42 @@
1
+ module Endeca
2
+
3
+ class Breadcrumb
4
+ include Readers
5
+
6
+ def self.create(raw)
7
+ name = raw['Type']
8
+ breadcrumb_class = self
9
+
10
+ if name
11
+ unless Breadcrumbs.include?(name)
12
+ raise Breadcrumbs::TypeError, "Unknown breadcrumb type: #{name.inspect}"
13
+ end
14
+ breadcrumb_class = Breadcrumbs[name]
15
+ end
16
+
17
+ breadcrumb_class.new(raw)
18
+ end
19
+
20
+ def self.to_proc
21
+ proc(&method(:create))
22
+ end
23
+
24
+ attr_reader :raw
25
+ def initialize(raw={})
26
+ @raw = raw
27
+ end
28
+ alias_method :attributes, :raw
29
+
30
+ reader 'Type' => :type
31
+ def name; '' end
32
+
33
+ def ==(other)
34
+ name == other.name
35
+ end
36
+
37
+ def inspect
38
+ "#<#{self.class}=0x#{self.object_id.to_s(16)} name=#{name.inspect}>"
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,13 @@
1
+ module Endeca
2
+ module Breadcrumbs
3
+ class TypeError < StandardError; end
4
+
5
+ def self.include?(klass)
6
+ self.const_defined?(klass)
7
+ end
8
+
9
+ def self.[](klass)
10
+ self.const_get(klass)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,38 @@
1
+ module Endeca
2
+ class Dimension
3
+ include Comparable
4
+ include Readers
5
+ extend ClassToProc
6
+
7
+ reader \
8
+ "DimValueName" => :name,
9
+ "SelectionLink" => :selection_link,
10
+ "RemovalLink" => :removal_link
11
+
12
+ integer_reader \
13
+ "DimValueID" => :id,
14
+ "NumberofRecords" => :record_count
15
+
16
+ attr_reader :raw
17
+ def initialize(raw={})
18
+ @raw=raw
19
+ end
20
+ alias_method :attributes, :raw
21
+
22
+ def to_endeca_params
23
+ selection_link || removal_link
24
+ end
25
+
26
+ def inspect
27
+ "#<#{self.class}=0x#{self.object_id.to_s(16)} id=#{id} name=#{name.inspect}>"
28
+ end
29
+
30
+ def ==(other)
31
+ id == other.id
32
+ end
33
+
34
+ def <=>(other)
35
+ name <=> other.name
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,146 @@
1
+ module Endeca
2
+ # Endeca Documents provide accessors for document properties
3
+ # returned by an Endeca query. Interesting Document properties must be
4
+ # declared with reader to be accessible on the object.
5
+ #
6
+ # The +reader+ declaration provided by Readers can also handle basic data transformations (i.e.
7
+ # typecasting) and a few basic examples are provided (i.e. +integer_reader+).
8
+ class Document
9
+ include Readers
10
+ extend ClassToProc
11
+ extend Transformer
12
+
13
+ inherited_accessor :mappings, {}
14
+ inherited_property :path
15
+ inherited_property :default_params, {}
16
+ inherited_property :collection_class, DocumentCollection
17
+
18
+ inherited_accessor :reader_names, []
19
+ def self.field_names; reader_names; end
20
+
21
+ reader :id
22
+
23
+ attr_reader :raw, :properties
24
+ def initialize(record_raw=nil)
25
+ @raw = record_raw || {}
26
+ @properties = @raw['Properties'] || {}
27
+ end
28
+
29
+ alias_method :attributes, :properties
30
+
31
+ def ==(other)
32
+ id == other.id
33
+ end
34
+
35
+ def self.inspect
36
+ return <<-INSPECT
37
+ #<#{self}>
38
+ Path: #{get_path.inspect}
39
+ Collection Class: #{get_collection_class.inspect}"
40
+ Mappings:\n\t#{mappings.collect{|k,v| "#{k}: #{v.inspect}\n\t"}.to_s}
41
+ DefaultParams:\n\t#{get_default_params.collect{|k,v| "#{k}: #{v.inspect}\n\t"}.to_s}
42
+ INSPECT
43
+ end
44
+
45
+ def inspect
46
+ "#<#{self.class}:0x#{self.object_id.to_s(16)}>"
47
+ end
48
+
49
+ # Returns the collection of Endeca::Dimension for the given Document
50
+ def dimensions
51
+ return @dimensions if @dimensions
52
+ @dimensions = {}
53
+ (raw['Dimensions'] || {}).each do |name, values|
54
+ values = [values] unless Array === values
55
+ @dimensions[name] = values.map(&Dimension)
56
+ end
57
+ @dimensions
58
+ end
59
+
60
+ # Find operates with three distinct retrieval approaches:
61
+ #
62
+ # * Find by id - This is a specific id (1) or id string ("1")
63
+ # * Find first - This will return the first record matching the query options
64
+ # used
65
+ # * Find all - This will return a collection of Documents matching the
66
+ # query options. This is the default behavior of find if only query options
67
+ # are passed.
68
+ #
69
+ # ==== Parameters
70
+ #
71
+ # Find accepts a query options hash. These options are either passed
72
+ # directly to Endeca or mapped (by use of +map+) to new parameters that are
73
+ # passed to Endeca.
74
+ #
75
+ # ==== Examples
76
+ #
77
+ # # find by id
78
+ # Listing.find(1) # returns the Document for ID = 1
79
+ # Listing.find('1') # returns the Document for ID = 1
80
+ #
81
+ # # find all
82
+ # Listing.find(:all) # returns a collection of Documents
83
+ # Listing.find(:all, :available => true)
84
+ #
85
+ # # find first
86
+ # Listing.find(:first) # Returns the first Document for the query
87
+ # Listing.find(:first, :available => true)
88
+ def self.find(what, query_options={})
89
+ case what
90
+ when Integer, /^[A-Z\d]+$/
91
+ by_id(what, query_options)
92
+ when String
93
+ all(what)
94
+ when :first
95
+ first(query_options)
96
+ when :all
97
+ all(query_options)
98
+ else
99
+ all(what)
100
+ end
101
+ end
102
+
103
+
104
+ # Returns the first Document matching the query options.
105
+ def self.first(query_options={})
106
+ response = request(query_options)
107
+ record = if response['AggrRecords']
108
+ response['AggrRecords'].first['Records'].first
109
+ elsif response['Records']
110
+ response['Records'].first
111
+ else
112
+ nil
113
+ end
114
+
115
+ record && new(record)
116
+ end
117
+
118
+ # Returns all Documents matching the query options.
119
+ def self.all(query_options={})
120
+ get_collection_class.new(request(query_options), self)
121
+ end
122
+
123
+ # Returns a Document by id
124
+ def self.by_id(id, query_options={})
125
+ first(query_options.merge(:id => id, :skip_default_endeca_parameters => true))
126
+ end
127
+
128
+ private
129
+
130
+ def self.request(query_options)
131
+ Endeca::Request.perform(get_path, parse_query_options(query_options))
132
+ end
133
+
134
+ def self.parse_query_options(query_options)
135
+ if query_options.respond_to?(:merge)
136
+ unless query_options.delete(:skip_default_endeca_parameters)
137
+ query_options = get_default_params.merge(query_options)
138
+ end
139
+
140
+ transform_query_options(query_options)
141
+ else
142
+ URI.unescape(query_options)
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,112 @@
1
+ module Endeca
2
+ # Endeca DocumentCollections wrap a collection of Endeca Documents to provide
3
+ # access to metadata returned by the Endeca query. They behave like a simple
4
+ # Array in most cases (e.g. iteration) but also provide access to
5
+ # +refinements+.
6
+ #
7
+ # ==Attribute Readers
8
+ #
9
+ # DocumentCollection provides attribute readers for collection metadata in
10
+ # an interface that is compatible with WillPaginate::Collection for use in
11
+ # views.
12
+ #
13
+ # == Method Delegation
14
+ #
15
+ # DocumentCollections delegate array-like behavior to their embedded document
16
+ # collection, (+documents+). In most cases a DocumentCollection can be used
17
+ # as if it were an array of Document objects. (Array delegation pattern
18
+ # borrowed from Rake::FileList)
19
+ class DocumentCollection
20
+ include Readers
21
+ extend ClassToProc
22
+
23
+ attr_reader :raw
24
+ def initialize(raw, document_klass=Document)
25
+ @raw = raw
26
+ @document_klass = document_klass
27
+ end
28
+
29
+ def attributes
30
+ @raw['MetaInfo'] || {}
31
+ end
32
+
33
+ reader \
34
+ 'NextPageLink' => :next_page_params
35
+
36
+ integer_reader \
37
+ 'NumberofPages' => :total_pages,
38
+ 'NumberofRecordsperPage' => :per_page,
39
+ 'PageNumber' => :current_page,
40
+ 'TotalNumberofMatchingRecords' => :total_entries
41
+
42
+ # WillPaginate +offset+ correspondes to Endeca StartingRecordNumber - 1
43
+ reader('StartingRecordNumber' => :offset) {|i| Integer(i) - 1}
44
+
45
+ # The previous page number.
46
+ # Returns nil if there is no previous page.
47
+ # Borrowed from WillPaginate for compatibility.
48
+ def previous_page
49
+ current_page > 1 ? (current_page - 1) : nil
50
+ end
51
+
52
+ # The next page number.
53
+ # Returns nil if there is no next page.
54
+ # Borrowed from WillPaginate for compatibility
55
+ def next_page
56
+ current_page < total_pages ? (current_page + 1) : nil
57
+ end
58
+
59
+ # The internal collection of Document objects. Array methods are delegated here.
60
+ def documents
61
+ if @raw['Records']
62
+ @documents ||= @raw['Records'].map(&@document_klass)
63
+ elsif aggregate?
64
+ @documents ||= @raw['AggrRecords'].map{|aggregate| aggregate['Records'].first}.map(&@document_klass)
65
+ else
66
+ []
67
+ end
68
+ end
69
+
70
+ def aggregate?
71
+ @raw['AggrRecords'] ? true : false
72
+ end
73
+
74
+ # The collection of Refinement objects for the collection.
75
+ def refinements
76
+ @refinements ||= (@raw['Refinements'] || []).map(&Refinement)
77
+ end
78
+
79
+ # The collection of Breadcrumb objects for the collection.
80
+ def breadcrumbs
81
+ @breadcrumbs ||= (@raw['Breadcrumbs'] || []).map(&Breadcrumb)
82
+ end
83
+
84
+ # Return the refinement by name
85
+ def refinement_by_name(name)
86
+ refinements.find{|ref| ref.name.downcase == name.downcase}
87
+ end
88
+
89
+ # List of array methods (that are not in +Object+) that need to be
90
+ # delegated to +documents+.
91
+ ARRAY_METHODS = (Array.instance_methods - Object.instance_methods).map { |n| n.to_s }
92
+
93
+ # List of additional methods that must be delegated to +documents+.
94
+ MUST_DEFINE = %w[to_a to_ary inspect]
95
+
96
+ (ARRAY_METHODS + MUST_DEFINE).uniq.each do |method|
97
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
98
+ def #{method}(*args, &block) # def each(*args, &block)
99
+ documents.send(:#{method}, *args, &block) # documents.send(:each, *args, &block)
100
+ end # end
101
+ RUBY
102
+ end
103
+
104
+ # Lie about our class. Borrowed from Rake::FileList
105
+ # Note: Does not work for case equality (<tt>===</tt>)
106
+ def is_a?(klass)
107
+ klass == Array || super(klass)
108
+ end
109
+ alias kind_of? is_a?
110
+
111
+ end
112
+ end
@@ -0,0 +1,9 @@
1
+ module Endeca
2
+ module Logging
3
+ # Log and benchmark the workings of a single block. Will only be called if
4
+ # Endeca.benchmark is true
5
+ def log(message)
6
+ Endeca.logger.debug(message) if Endeca.debug && Endeca.logger
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,191 @@
1
+ module Endeca
2
+ class Map
3
+ def initialize(old_key, new_key=nil)
4
+ @old_key = old_key
5
+ @new_key = new_key || @old_key
6
+ boolean
7
+ end
8
+
9
+ # Convert true and false into their Endeca equivalents
10
+ def boolean
11
+ @boolean = true
12
+ add_transformation { |value| value == true ? 1 : value }
13
+ add_transformation { |value| value == false ? 0 : value }
14
+ self
15
+ end
16
+
17
+ def boolean?; @boolean end
18
+
19
+ # Mapping actually resides in another key, value pair. Uses Endeca default
20
+ # join characters (can be overridden by specifying +with+ and/or +join+).
21
+ #
22
+ # Example:
23
+ # map(:city => :propertycity).into(:ntk => :ntt)
24
+ #
25
+ # Document.all(:city => 'Atlanta') =>
26
+ # ?ntk=propercity&ntt=>Atlanta
27
+ def into(hash)
28
+ hash = hash.intern if hash.respond_to?(:intern)
29
+ if hash.is_a?(Hash)
30
+ raise ArgumentError, "Only one key/value pair allowed" if hash.size > 1
31
+ hash = hash.to_a.flatten
32
+ hash = {hash.first.to_sym => hash.last.to_sym}
33
+ end
34
+ @into = hash
35
+ with ':'
36
+ join '|'
37
+ self
38
+ end
39
+
40
+ def into?; @into end
41
+
42
+ # Mapping actually resides in another key, value pair. Uses Endeca default
43
+ # join characters (can be overridden by specifying +with+ and/or +join+).
44
+ #
45
+ # Example:
46
+ # map(:city => :propertycity).split_into(:ntk => :ntt)
47
+ #
48
+ # Document.all(:city => 'Atlanta, New York, Los Angeles') =>
49
+ # ?ntk=propercity|propertycity|propertycity&ntt=>Atlanta|New York|Los Angeles
50
+ def split_into(hash, split_value = ',')
51
+ into(hash)
52
+ @split_value = split_value
53
+ end
54
+
55
+ # When mapping multiple key/value pairs to a single parameter value (via
56
+ # +into+), use this character to join a key with a value.
57
+ def with(character)
58
+ @with = character
59
+ self
60
+ end
61
+
62
+ # When mapping multiple key/value pairs to one or two parameter values (via
63
+ # +into+), use this character to join each pair.
64
+ def join(character)
65
+ @join = character
66
+ self
67
+ end
68
+
69
+ def join?; @join end
70
+
71
+ # Code block to execute on the original data
72
+ def transform(&block)
73
+ add_transformation block
74
+ self
75
+ end
76
+
77
+ # When mapping multiple values, enclose the values in the string provided
78
+ # to +enclose+.
79
+ def enclose(str)
80
+ @enclose = str
81
+ self
82
+ end
83
+
84
+ def enclose?; @enclose end
85
+
86
+ # When mapping multiple key/value pairs, replace existing keys with the new
87
+ # keys rather than joining.
88
+ def replace!
89
+ @replace = true
90
+ self
91
+ end
92
+
93
+ def replace?; @replace end
94
+
95
+ # Perform the mapping as defined for the current_query
96
+ def perform(current_query)
97
+ @current_query = current_query.symbolize_keys
98
+ @current_value = @current_query[@old_key]
99
+
100
+ perform_transformation
101
+ perform_map
102
+ perform_into
103
+ perform_join
104
+
105
+ return @new_query
106
+ end
107
+
108
+ # Mapping object is equal to other mapping object if their attributes
109
+ # are equal
110
+ def ==(other)
111
+ equality_elements == other.equality_elements
112
+ end
113
+
114
+ def inspect
115
+ perform({ 'old_key' => "inspect_data" }).inspect
116
+ end
117
+
118
+ private
119
+
120
+ def transformations
121
+ @transformations ||= []
122
+ end
123
+
124
+ def transformations?
125
+ !transformations.empty?
126
+ end
127
+
128
+ def add_transformation(transformation = nil, &block)
129
+ transformations.push transformation if transformation
130
+ transformations.push block if block_given?
131
+ end
132
+
133
+ def perform_transformation
134
+ return unless transformations?
135
+ current_value = @current_value
136
+ transformations.each do |transformation|
137
+ current_value = transformation.call(current_value)
138
+ end
139
+ @current_value = current_value
140
+ end
141
+
142
+ def perform_map
143
+ @new_query = {@new_key => @current_value}.symbolize_keys
144
+ end
145
+
146
+ def perform_into
147
+ return unless into?
148
+
149
+ old_key, old_value = Array(@new_query).flatten
150
+ new_key, new_value = Array(@into).flatten
151
+
152
+ if new_value
153
+ if @split_value
154
+ @new_query = perform_split(old_key, old_value, new_key, new_value)
155
+ else
156
+ @new_query = {new_key => old_key, new_value => old_value}
157
+ end
158
+ else
159
+ new_value = [old_key, old_value].compact.join(@with)
160
+ new_value = "#{@enclose}(#{new_value})" if enclose?
161
+ @new_query = {new_key => new_value}
162
+ end
163
+ end
164
+
165
+ def perform_join
166
+ return unless join?
167
+ return if replace?
168
+
169
+ @new_query.each do |key, value|
170
+ @new_query[key] = [@current_query[key], value].compact.join(@join)
171
+ end
172
+ end
173
+
174
+ protected
175
+
176
+ def perform_split(old_key, old_value, new_key, new_value)
177
+ key = []
178
+ value = []
179
+ old_value.split(@split_value).each do |val|
180
+ key << old_key
181
+ value << val
182
+ end
183
+
184
+ {new_key => key.join(@join), new_value => value.join(@join)}
185
+ end
186
+
187
+ def equality_elements # :nodoc:
188
+ [@old_key, @new_key, @join, @with, @join]
189
+ end
190
+ end
191
+ end