primedia-endeca 0.9.0
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 +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
|