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