primedia-endeca 0.9.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.
- data/History.txt +4 -0
- data/Manifest.txt +31 -0
- data/README.rdoc +60 -0
- data/Rakefile +48 -0
- data/example/benchmark.rb +13 -0
- data/example/listing.rb +36 -0
- data/lib/class_to_proc.rb +5 -0
- data/lib/core_ext.rb +73 -0
- data/lib/endeca.rb +38 -0
- data/lib/endeca/dimension.rb +30 -0
- data/lib/endeca/document.rb +109 -0
- data/lib/endeca/document_collection.rb +93 -0
- data/lib/endeca/map.rb +119 -0
- data/lib/endeca/readers.rb +95 -0
- data/lib/endeca/refinement.rb +29 -0
- data/lib/endeca/request.rb +58 -0
- data/lib/endeca/transformer.rb +40 -0
- data/spec/core_ext_spec.rb +52 -0
- data/spec/endeca/dimension_spec.rb +65 -0
- data/spec/endeca/document_collection_spec.rb +116 -0
- data/spec/endeca/document_spec.rb +290 -0
- data/spec/endeca/map_spec.rb +81 -0
- data/spec/endeca/readers_spec.rb +112 -0
- data/spec/endeca/refinement_spec.rb +38 -0
- data/spec/endeca/request_spec.rb +65 -0
- data/spec/endeca/transformer_spec.rb +50 -0
- data/spec/endeca_spec.rb +12 -0
- data/spec/rcov.opts +5 -0
- data/spec/spec.opts +3 -0
- data/spec/spec_helper.rb +1 -0
- metadata +93 -0
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'endeca/document'
|
2
|
+
module Endeca
|
3
|
+
# Endeca DocumentCollections wrap a collection of Endeca Documents to provide
|
4
|
+
# access to metadata returned by the Endeca query. They behave like a simple
|
5
|
+
# Array in most cases (e.g. iteration) but also provide access to
|
6
|
+
# +refinements+.
|
7
|
+
#
|
8
|
+
# ==Attribute Readers
|
9
|
+
#
|
10
|
+
# DocumentCollection provides attribute readers for collection metadata in
|
11
|
+
# an interface that is compatible with WillPaginate::Collection for use in
|
12
|
+
# views.
|
13
|
+
#
|
14
|
+
# == Method Delegation
|
15
|
+
#
|
16
|
+
# DocumentCollections delegate array-like behavior to their embedded document
|
17
|
+
# collection, (+documents+). In most cases a DocumentCollection can be used
|
18
|
+
# as if it were an array of Document objects. (Array delegation pattern
|
19
|
+
# borrowed from Rake::FileList)
|
20
|
+
class DocumentCollection
|
21
|
+
extend ClassToProc
|
22
|
+
extend Readers
|
23
|
+
|
24
|
+
attr_reader :raw
|
25
|
+
def initialize(raw, document_klass=Document)
|
26
|
+
@raw = raw
|
27
|
+
@document_klass = document_klass
|
28
|
+
end
|
29
|
+
|
30
|
+
def attributes
|
31
|
+
@raw['MetaInfo'] || {}
|
32
|
+
end
|
33
|
+
|
34
|
+
reader \
|
35
|
+
'NextPageLink' => :next_page_params
|
36
|
+
|
37
|
+
integer_reader \
|
38
|
+
'NumberofPages' => :total_pages,
|
39
|
+
'NumberofRecordsperPage' => :per_page,
|
40
|
+
'PageNumber' => :current_page,
|
41
|
+
'TotalNumberofMatchingRecords' => :total_entries
|
42
|
+
|
43
|
+
# WillPaginate +offset+ correspondes to Endeca StartingRecordNumber - 1
|
44
|
+
reader('StartingRecordNumber' => :offset) {|i| Integer(i) - 1}
|
45
|
+
|
46
|
+
# The previous page number.
|
47
|
+
# Returns nil if there is no previous page.
|
48
|
+
# Borrowed from WillPaginate for compatibility.
|
49
|
+
def previous_page
|
50
|
+
current_page > 1 ? (current_page - 1) : nil
|
51
|
+
end
|
52
|
+
|
53
|
+
# The next page number.
|
54
|
+
# Returns nil if there is no next page.
|
55
|
+
# Borrowed from WillPaginate for compatibility
|
56
|
+
def next_page
|
57
|
+
current_page < total_pages ? (current_page + 1) : nil
|
58
|
+
end
|
59
|
+
|
60
|
+
# The internal collection of Document objects. Array methods are delegated here.
|
61
|
+
def documents
|
62
|
+
@documents ||= (@raw['Records'] || []).map(&@document_klass)
|
63
|
+
end
|
64
|
+
|
65
|
+
# The collection of Refinement objects for the collection.
|
66
|
+
def refinements
|
67
|
+
@refinements ||= (@raw['Refinements'] || []).map(&Refinement)
|
68
|
+
end
|
69
|
+
|
70
|
+
# List of array methods (that are not in +Object+) that need to be
|
71
|
+
# delegated to +documents+.
|
72
|
+
ARRAY_METHODS = (Array.instance_methods - Object.instance_methods).map { |n| n.to_s }
|
73
|
+
|
74
|
+
# List of additional methods that must be delegated to +documents+.
|
75
|
+
MUST_DEFINE = %w[to_a to_ary inspect]
|
76
|
+
|
77
|
+
(ARRAY_METHODS + MUST_DEFINE).uniq.each do |method|
|
78
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
79
|
+
def #{method}(*args, &block) # def each(*args, &block)
|
80
|
+
documents.send(:#{method}, *args, &block) # documents.send(:each, *args, &block)
|
81
|
+
end # end
|
82
|
+
RUBY
|
83
|
+
end
|
84
|
+
|
85
|
+
# Lie about our class. Borrowed from Rake::FileList
|
86
|
+
# Note: Does not work for case equality (<tt>===</tt>)
|
87
|
+
def is_a?(klass)
|
88
|
+
klass == Array || super(klass)
|
89
|
+
end
|
90
|
+
alias kind_of? is_a?
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
data/lib/endeca/map.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
module Endeca
|
2
|
+
class Map
|
3
|
+
def initialize(old_key=nil, new_key=nil)
|
4
|
+
@old_key = old_key
|
5
|
+
@new_key = new_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).in(:ntk => ntt)
|
24
|
+
#
|
25
|
+
# Document.all(:city => 'Atlanta') =>
|
26
|
+
# ?ntk=propercity&ntt=>Atlanta
|
27
|
+
def into(hash)
|
28
|
+
@into = hash
|
29
|
+
@with ||= ':'
|
30
|
+
@join ||= '|'
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
# When mapping multiple key/value pairs to a single parameter value (via
|
35
|
+
# +into+), use this character to join a key with a value.
|
36
|
+
def with(character)
|
37
|
+
@with = character
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
# When mapping multiple key/value pairs to one or two parameter values (via
|
42
|
+
# +into+), use this character to join each pair.
|
43
|
+
def join(character)
|
44
|
+
@join = character
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
# Code block to execute on the original data
|
49
|
+
def transform(&block)
|
50
|
+
add_transformation block
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
# Perform the mapping as defined for the current_query
|
55
|
+
def perform(current_query)
|
56
|
+
@current_query = current_query
|
57
|
+
|
58
|
+
perform_transformation
|
59
|
+
perform_map
|
60
|
+
perform_into
|
61
|
+
perform_join
|
62
|
+
|
63
|
+
return @new_query
|
64
|
+
end
|
65
|
+
|
66
|
+
# Mapping object is equal to other mapping object if their attributes
|
67
|
+
# are equal
|
68
|
+
def ==(other)
|
69
|
+
equality_elements == other.equality_elements
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def transformations
|
75
|
+
@transformations ||= []
|
76
|
+
end
|
77
|
+
|
78
|
+
def add_transformation(transformation = nil, &block)
|
79
|
+
transformations.push transformation if transformation
|
80
|
+
transformations.push block if block_given?
|
81
|
+
end
|
82
|
+
|
83
|
+
def perform_transformation
|
84
|
+
current_value = @current_query[@old_key]
|
85
|
+
transformations.each do |transformation|
|
86
|
+
current_value = transformation.call(current_value)
|
87
|
+
end
|
88
|
+
@current_value = current_value
|
89
|
+
end
|
90
|
+
|
91
|
+
def perform_map
|
92
|
+
@new_query = {@new_key => @current_value}
|
93
|
+
end
|
94
|
+
|
95
|
+
def perform_into
|
96
|
+
return unless @into
|
97
|
+
old_key, old_value = Array(@new_query).flatten
|
98
|
+
new_key, new_value = Array(@into).flatten
|
99
|
+
if new_value
|
100
|
+
@new_query = {new_key => old_key, new_value => old_value}
|
101
|
+
else
|
102
|
+
@new_query = {new_key => [old_key, old_value].compact.join(@with)}
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def perform_join
|
107
|
+
return unless @join
|
108
|
+
@new_query.each do |key, value|
|
109
|
+
@new_query[key] = [@current_query[key], value].compact.join(@join)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
protected
|
114
|
+
|
115
|
+
def equality_elements # :nodoc:
|
116
|
+
[@old_key, @new_key, @join, @with, @join]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Endeca
|
2
|
+
class ReaderError < ::StandardError; end
|
3
|
+
|
4
|
+
module Readers
|
5
|
+
def add_reader(name, &block)
|
6
|
+
meta = (class << self; self; end)
|
7
|
+
meta.instance_eval do
|
8
|
+
define_method(name) { |*attrs| reader(*attrs, &block) }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# Maps key/value pairs from the data structure used to initialize a
|
13
|
+
# Endeca object. Allows attribute renaming. Use a block to perform data
|
14
|
+
# injunction on the value as it is set.
|
15
|
+
#
|
16
|
+
# ==== Examples
|
17
|
+
#
|
18
|
+
# # Specify basic attributes
|
19
|
+
# reader :title
|
20
|
+
#
|
21
|
+
# # Attribute renaming
|
22
|
+
# reader :long_desc => :description
|
23
|
+
#
|
24
|
+
# # Data injunction
|
25
|
+
# reader(:title => :upcased_title) { |title| title.upcase }
|
26
|
+
def reader(*attrs,&block)
|
27
|
+
hash = {}
|
28
|
+
block ||= lambda {|x| x}
|
29
|
+
|
30
|
+
hash.update(attrs.pop) if Hash === attrs.last
|
31
|
+
|
32
|
+
attrs.each{ |attr| hash[attr] = attr }
|
33
|
+
|
34
|
+
hash.each do |variable, method|
|
35
|
+
define_method(method) do
|
36
|
+
begin
|
37
|
+
block.call(attributes[variable.to_s])
|
38
|
+
rescue StandardError => e
|
39
|
+
raise Endeca::ReaderError, e.message
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Typecasts attributes as integers.
|
46
|
+
#
|
47
|
+
# ==== Examples
|
48
|
+
# integer_reader :id, :rating
|
49
|
+
def integer_reader(*attrs)
|
50
|
+
reader(*attrs) { |value| Integer(value) }
|
51
|
+
end
|
52
|
+
|
53
|
+
# Typecasts attributes as BigDecimal
|
54
|
+
#
|
55
|
+
# ==== Examples
|
56
|
+
# decimal_reader :price
|
57
|
+
def decimal_reader(*attrs)
|
58
|
+
require 'bigdecimal' unless defined?(BigDecimal)
|
59
|
+
reader(*attrs) { |value| BigDecimal(value.to_s) }
|
60
|
+
end
|
61
|
+
|
62
|
+
# Typecasts attributes as floats
|
63
|
+
#
|
64
|
+
# ==== Examples
|
65
|
+
# float_reader :latitude, :longitude
|
66
|
+
def float_reader(*attrs)
|
67
|
+
reader(*attrs) { |value| Float(value) }
|
68
|
+
end
|
69
|
+
|
70
|
+
# Typecasts attributes as a Perly boolean ("0" == false, "1" == true")
|
71
|
+
#
|
72
|
+
# ==== Examples
|
73
|
+
# boolean_reader :price
|
74
|
+
def boolean_reader(*attrs)
|
75
|
+
reader(*attrs) { |value| value == "1" ? true : false }
|
76
|
+
end
|
77
|
+
|
78
|
+
def dim_reader(*attrs, &block)
|
79
|
+
hash = {}
|
80
|
+
block ||= lambda {|x| x}
|
81
|
+
|
82
|
+
hash.update(attrs.pop) if Hash === attrs.last
|
83
|
+
|
84
|
+
attrs.each{|attr| hash[attr] = attr}
|
85
|
+
|
86
|
+
hash.each do |variable, method|
|
87
|
+
define_method method do
|
88
|
+
dim = dimensions[variable.to_s]
|
89
|
+
name = dim && dim.name
|
90
|
+
block.call(name)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Endeca
|
2
|
+
class Refinement
|
3
|
+
extend ClassToProc
|
4
|
+
extend Readers
|
5
|
+
|
6
|
+
attr_reader :raw
|
7
|
+
def initialize(raw={})
|
8
|
+
@raw = raw
|
9
|
+
end
|
10
|
+
|
11
|
+
def ==(other)
|
12
|
+
id == other.id
|
13
|
+
end
|
14
|
+
|
15
|
+
def inspect
|
16
|
+
"#<#{self.class}=0x#{self.object_id.to_s(16)} id=#{id} name=#{name.inspect}>"
|
17
|
+
end
|
18
|
+
|
19
|
+
def attributes
|
20
|
+
(@raw['Dimensions'] || []).first || {}
|
21
|
+
end
|
22
|
+
|
23
|
+
reader \
|
24
|
+
'DimensionName' => :name,
|
25
|
+
'ExpansionLink' => :to_params
|
26
|
+
|
27
|
+
integer_reader 'DimensionID' => :id
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'uri'
|
2
|
+
module Endeca
|
3
|
+
class RequestError < ::StandardError; end
|
4
|
+
|
5
|
+
class Request
|
6
|
+
|
7
|
+
def self.perform(path, query=nil)
|
8
|
+
new(path, query).perform
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(path, query=nil)
|
12
|
+
@path = path.strip
|
13
|
+
@query = query
|
14
|
+
end
|
15
|
+
|
16
|
+
def perform
|
17
|
+
handle_response(get_response)
|
18
|
+
end
|
19
|
+
|
20
|
+
def uri
|
21
|
+
return @uri if @uri
|
22
|
+
|
23
|
+
@uri = URI.parse(@path)
|
24
|
+
@uri.query = query_string
|
25
|
+
@uri
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def get_response #:nodoc:
|
31
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
32
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
33
|
+
|
34
|
+
Endeca.logger.debug "ENDECA REQUEST: uri=#{uri}" if Endeca.debug
|
35
|
+
http.request(request)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Raises exception Net::XXX (http error code) if an http error occured
|
39
|
+
def handle_response(response) #:nodoc:
|
40
|
+
case response
|
41
|
+
when Net::HTTPSuccess
|
42
|
+
JSON.parse(response.body)
|
43
|
+
else
|
44
|
+
response.error! # raises exception corresponding to http error Net::XXX
|
45
|
+
end
|
46
|
+
|
47
|
+
rescue => e
|
48
|
+
raise RequestError, e.message
|
49
|
+
end
|
50
|
+
|
51
|
+
def query_string
|
52
|
+
query_string_parts = [@uri.query, @query.to_params]
|
53
|
+
query_string_parts.reject!{ |s| s.nil? || s.empty? }
|
54
|
+
|
55
|
+
query_string_parts.empty? ? nil : query_string_parts.join('&')
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Endeca
|
2
|
+
module Transformer
|
3
|
+
# Requires existence of mappings accessor (Hash)
|
4
|
+
#
|
5
|
+
# ==== Examples
|
6
|
+
# # Standard map call that returns an Endeca::Map object
|
7
|
+
# map(:old_name => :new_name)
|
8
|
+
#
|
9
|
+
# # Allows to to create a map object to perform other functionality such
|
10
|
+
# # as transformations.
|
11
|
+
# map(:new_name)
|
12
|
+
def map(mapping = {})
|
13
|
+
mapping = {mapping => mapping} if Symbol === mapping
|
14
|
+
|
15
|
+
if mapping.length > 1
|
16
|
+
raise ArgumentError, "map only accepts one key=>value pair"
|
17
|
+
end
|
18
|
+
|
19
|
+
mapping.each do |key, transformed_key|
|
20
|
+
transformed_key = key unless transformed_key
|
21
|
+
return mappings[key] = Map.new(key, transformed_key)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Use the mappings hash to replace domain level query query_options with
|
26
|
+
# their Endeca equivalent.
|
27
|
+
def transform_query_options(query_options)
|
28
|
+
query = query_options.dup
|
29
|
+
query.each do |key, value|
|
30
|
+
if mappings[key]
|
31
|
+
new_options = mappings[key].perform(query_options)
|
32
|
+
query_options.delete(key)
|
33
|
+
query_options.update(new_options)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
query_options
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), %w[spec_helper])
|
2
|
+
|
3
|
+
describe Array do
|
4
|
+
describe "#to_params" do
|
5
|
+
it "should join the elements with ampersand" do
|
6
|
+
["foo=1","bar=3"].to_params.should == "foo=1&bar=3"
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should escape all elements" do
|
10
|
+
['|=|','||=||'].to_params.should == '%7C=%7C&%7C%7C=%7C%7C'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe Hash do
|
16
|
+
describe "#to_params" do
|
17
|
+
it "should join a key-value pair with equals" do
|
18
|
+
{:foo => :bar}.to_params.should == 'foo=bar'
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should join two key-value pairs with ampersand" do
|
22
|
+
result = {:foo => :bar, :bizz => :bazz}.to_params
|
23
|
+
(result == 'foo=bar&bizz=bazz' || result == 'bizz=bazz&foo=bar').should be_true
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should use brackets to indicate a nested hash" do
|
27
|
+
{:foo => {:foo => :bar}}.to_params.should == 'foo[foo]=bar'
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should escape all elements" do
|
31
|
+
{'|' => '||'}.to_params.should == '%7C=%7C%7C'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe NilClass do
|
37
|
+
describe "to_params" do
|
38
|
+
it "should return the empty string" do
|
39
|
+
nil.to_params.should == ''
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe String do
|
45
|
+
describe "#to_params" do
|
46
|
+
it "should URI escape the contents" do
|
47
|
+
'|'.to_params.should == '%7C'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# EOF
|