endeca 1.3.7
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/Manifest.txt +37 -0
- data/README.rdoc +60 -0
- data/Rakefile +52 -0
- data/endeca.gemspec +20 -0
- data/example/benchmark.rb +13 -0
- data/example/listing.rb +33 -0
- data/lib/class_to_proc.rb +5 -0
- data/lib/core_ext.rb +106 -0
- data/lib/endeca.rb +76 -0
- data/lib/endeca/benchmarking.rb +38 -0
- data/lib/endeca/breadcrumb.rb +42 -0
- data/lib/endeca/breadcrumbs.rb +13 -0
- data/lib/endeca/dimension.rb +38 -0
- data/lib/endeca/document.rb +146 -0
- data/lib/endeca/document_collection.rb +112 -0
- data/lib/endeca/logging.rb +9 -0
- data/lib/endeca/map.rb +191 -0
- data/lib/endeca/readers.rb +93 -0
- data/lib/endeca/refinement.rb +42 -0
- data/lib/endeca/refinement_dimension.rb +32 -0
- data/lib/endeca/request.rb +88 -0
- data/lib/endeca/transformer.rb +43 -0
- data/spec/core_ext_spec.rb +134 -0
- data/spec/endeca/benchmarking_spec.rb +33 -0
- data/spec/endeca/breadcrumb_spec.rb +90 -0
- data/spec/endeca/dimension_spec.rb +91 -0
- data/spec/endeca/document_collection_spec.rb +158 -0
- data/spec/endeca/document_spec.rb +378 -0
- data/spec/endeca/map_spec.rb +122 -0
- data/spec/endeca/readers_spec.rb +118 -0
- data/spec/endeca/refinement_dimension_spec.rb +74 -0
- data/spec/endeca/refinement_spec.rb +72 -0
- data/spec/endeca/request_spec.rb +107 -0
- data/spec/endeca/transformer_spec.rb +50 -0
- data/spec/endeca_spec.rb +37 -0
- data/spec/rcov.opts +5 -0
- data/spec/spec.opts +3 -0
- data/spec/spec_helper.rb +1 -0
- metadata +97 -0
@@ -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,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
|
data/lib/endeca/map.rb
ADDED
@@ -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
|