endeca 1.3.7

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,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