drawbridge 0.2.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.
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Drawbridge
3
+ class RefinementScrubber
4
+
5
+ def self.scrub(data)
6
+ Array(data).each do |ref|
7
+ add_aggrbins_key(ref) if ref['dimensionvalues'].present?
8
+ end
9
+ end
10
+
11
+ private
12
+
13
+ def self.add_aggrbins_key(ref)
14
+ ref['dimensionvalues'].each do |val|
15
+ next unless has_aggr_bins?(val)
16
+ val['dimvalueproperties']['aggrbins'] = val['dimvalueproperties']['dgraph.aggrbins']
17
+ end
18
+ end
19
+
20
+ def self.has_aggr_bins?(value)
21
+ props = value['dimvalueproperties']
22
+ value.present? && props.present? && props['dgraph.aggrbins'].present?
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,92 @@
1
+ # -*- encoding: utf-8 -*-require 'oj'
2
+ require 'oj'
3
+ require 'curb'
4
+
5
+ module Drawbridge
6
+
7
+ class Request
8
+ class RequestError < StandardError; end
9
+
10
+ ERROR_CODE = 0
11
+ ResultsError = {'status' => ERROR_CODE}
12
+
13
+ attr_accessor :uri, :query, :timeout
14
+
15
+ def self.perform(uri, query, timeout = (Drawbridge.config.timeout || 5))
16
+ raise RequestError, "Must provide a uri" unless uri
17
+ new(uri, query, timeout).perform
18
+ end
19
+
20
+ # Initialize
21
+ # @params: uri
22
+ # @params: query
23
+ # @params: timeout
24
+ def initialize(uri, query, timeout)
25
+ @uri = uri
26
+ @query = query
27
+ @timeout = timeout
28
+ end
29
+
30
+ # Perform HTTP call
31
+ # @return: json response
32
+ def perform
33
+ @perform ||= handle_response get_response
34
+ end
35
+
36
+ private
37
+
38
+ # Build full qualified uri
39
+ # @instance variable: query
40
+ # @instance variable: uri
41
+ def full_uri
42
+ @full_uri ||= query ? URI.escape("#{uri}?#{query}") : uri
43
+ end
44
+
45
+ # Make get request to assembler
46
+ # @instance variable: full_url
47
+ # @sucess: json data
48
+ # @failed: raising RequestError
49
+ def get_response
50
+ Drawbridge::Debug.log "ENDECA_QUERY", full_uri
51
+
52
+ Curl::Easy.perform(full_uri) do |curl|
53
+ curl.timeout = timeout
54
+ end
55
+ rescue Curl::Err::TimeoutError => e
56
+ raise e, e.message
57
+ rescue StandardError => e
58
+ puts "Request to: #{full_uri} failed: #{e.message}"
59
+ raise RequestError, e.message
60
+ end
61
+
62
+ # Handle response from assembler
63
+ # @success: Parse data with Oj gem
64
+ # @failed: defaul json {'status' => 0}
65
+ def handle_response(response)
66
+ case response.response_code.to_s
67
+ when "200"
68
+ begin
69
+ Oj.load(clean_bad_json(response.body_str))
70
+ rescue Oj::ParseError => e
71
+ raise RequestError, "JSON Parse error: #{e}"
72
+ end
73
+ else
74
+ puts "Unsuccessful response from #{full_uri}: #{response}\nbody:#{response.body_str unless response.nil?}"
75
+ Request::ResultsError
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def clean_bad_json(str)
82
+ str.encode!('UTF-16', 'UTF-8', :invalid => :replace, :replace => '')
83
+ str.encode!('UTF-8', 'UTF-16')
84
+ str.gsub!("\t",'')
85
+ str.gsub!("\\ ","\\\\\\ ")
86
+ str.gsub!(/\'/, "&#39;") unless Drawbridge.config.skip_single_quote_encoding
87
+ str
88
+ end
89
+
90
+ end
91
+
92
+ end
@@ -0,0 +1,146 @@
1
+ module Drawbridge
2
+ class Result
3
+
4
+ attr_accessor :data
5
+
6
+ def initialize(params)
7
+ @data = may_be_success_response params
8
+ end
9
+
10
+ def refinements
11
+ @refinements ||= RefinementScrubber.scrub(downcased_refinements)
12
+ end
13
+
14
+ def breadcrumbs
15
+ Array(downcase_element(data["Breadcrumbs"]))
16
+ end
17
+
18
+ def meta_info
19
+ downcase_element(data["MetaInfo"]) || {}
20
+ end
21
+
22
+ def records
23
+ @records ||= parse_records_array
24
+ end
25
+
26
+ def status
27
+ data['status'] || 1
28
+ end
29
+
30
+ def to_stats_hash
31
+ { meta_info: meta_info, breadcrumbs: breadcrumbs, refinements: refinements }
32
+ end
33
+
34
+ private
35
+
36
+ def downcased_refinements
37
+ downcase_array(@data['Refinements'])
38
+ end
39
+
40
+ def parse_records_array
41
+ is_aggregate? ? parse_aggregate_records : parse_records
42
+ end
43
+
44
+ def parse_aggregate_records
45
+ aggregate_records.map { |aggregate_record| merge_derived_properties aggregate_record }
46
+ end
47
+
48
+ def merge_derived_properties(aggregate_record)
49
+ first_parsed_record = parse_records(aggregate_record["Records"]).first
50
+ derived_properties = aggregate_record["DerivedProperties"]
51
+ first_parsed_record.merge derived_properties
52
+ end
53
+
54
+ def parse_records(records = unaggregate_records)
55
+ mod_records = records.map do |record|
56
+ record["Dimensions"].merge record['Properties']
57
+ end
58
+ special_handling mod_records
59
+ end
60
+
61
+ def is_aggregate?
62
+ data.has_key? 'AggrRecords'
63
+ end
64
+
65
+ def aggregate_records
66
+ Array(data['AggrRecords'])
67
+ end
68
+
69
+ def unaggregate_records
70
+ Array(data['Records'])
71
+ end
72
+
73
+ def downcase_hash(subject_hash)
74
+ return {} if subject_hash.nil?
75
+ subject_hash.each_with_object({}) { |(k, v), h| h[k.downcase] = downcase_element(v) }
76
+ end
77
+
78
+ def downcase_array(subject_array)
79
+ Array(subject_array).map { |element| downcase_element(element) }
80
+ end
81
+
82
+ def downcase_element(element)
83
+ if element.instance_of? Array
84
+ downcase_array element
85
+ elsif element.instance_of? Hash
86
+ downcase_hash element
87
+ else
88
+ element
89
+ end
90
+ end
91
+
92
+ def special_handling(records)
93
+ records.each do |record|
94
+ record['miles_to_geocode'] = parse_geocode record, 'miles'
95
+ record['kilometers_to_geocode'] = parse_geocode record, 'kilometers'
96
+ record['matched_on'] = parse_matched_on record
97
+ end
98
+ end
99
+
100
+ def parse_geocode(record, units)
101
+ units_to_geocode = /#{units}_to_geocode/
102
+ record[record.keys.detect{ |key| key =~ units_to_geocode }]
103
+ end
104
+
105
+ def parse_matched_on(record)
106
+ whymatch = record['DGraph.WhyDidItMatch']
107
+ return unless whymatch.to_s.include?(':')
108
+
109
+ if whymatch.instance_of? String
110
+ whymatch_string_to_hash whymatch
111
+ else
112
+ whymatch_array_to_hash whymatch
113
+ end
114
+ end
115
+
116
+ def whymatch_string_to_hash(str)
117
+ parts = str.split(':').map(&:strip)
118
+ { parts[0] => parts[1] }
119
+ end
120
+
121
+ def whymatch_array_to_hash(arr)
122
+ arr.each_with_object({}) do |s, hash|
123
+ arr = s.split(':').collect(&:strip)
124
+ hash[arr[0]] = arr[1]
125
+ end
126
+ end
127
+
128
+ # If Error returned by Endeca, return default structure
129
+ def may_be_success_response(params)
130
+ return params unless params['methodResponse']
131
+
132
+ status = response_params_contain_fault?(params) ? 404 : 500
133
+
134
+ { 'MetaInfo' => {}, 'Refinements' => [], 'Breadcrumbs' => [], 'AggrRecords' => [], 'status' => status }
135
+ end
136
+
137
+ def response_params_contain_fault?(params)
138
+ fault_string_from_params(params) =~ /HTTP Error 404/i
139
+ end
140
+
141
+ def fault_string_from_params(params)
142
+ params['methodResponse']['fault']['value']['faultString']
143
+ end
144
+
145
+ end
146
+ end
@@ -0,0 +1,190 @@
1
+ module Drawbridge
2
+ module Transformer
3
+ KM = 0.62137
4
+ PLUS = '+'
5
+ PIPE = '|'
6
+
7
+ def transform(adapter)
8
+ query = []
9
+
10
+ clean_adapter_keys(adapter).keys.each do |key|
11
+ begin
12
+ query << send(key, adapter[key])
13
+ rescue NoMethodError => e
14
+ raise NoMethodError, "Method #{key} is not supported"
15
+ end
16
+ end
17
+ query.compact!
18
+
19
+ Drawbridge::Debug.log "ENDECA PARAMS", query
20
+
21
+ query.join('&')
22
+ end
23
+
24
+ private
25
+
26
+ def clean_adapter_keys(adapter)
27
+ return {} if adapter.nil?
28
+
29
+ adapter[:rollup] && adapter[:rollup].blank? ?
30
+ adapter.delete(:aggregate_offset) :
31
+ adapter.delete(:offset)
32
+
33
+ adapter
34
+ end
35
+
36
+ # N - root navigation request, or one or more dimension value ID
37
+ # N=123+234
38
+ # @params Array
39
+ def root_navigtion(filters)
40
+ filters = [0] if filters.blank?
41
+
42
+ "N=#{filters.join(PLUS)}"
43
+ end
44
+
45
+ # Ns - sort keys
46
+ # @params - Array of Hashes [{'showapartments' => 1}, {'searchonly' => 1}]
47
+ # Ns=showapartments|1||searchonly|1
48
+ def order(sort_order)
49
+ return nil if sort_order.blank?
50
+
51
+ separator = '||'
52
+ sort_arr = []
53
+ sort_order.each do |item|
54
+ item.each_pair{|k,v| sort_arr << "#{k}#{PIPE}#{v}"}
55
+ end
56
+
57
+ "Ns=#{sort_arr.join(separator)}"
58
+ end
59
+
60
+ # Nf - Range filter
61
+ # key|GCLY latitude,longitude radius in Kilometers
62
+ # @params - Hash {lat: 123, lng: -12, radius: 10}
63
+ # Nf=geocode|GCLY 42.365615,-71.075647 10
64
+ def geo_filter(geo)
65
+ return nil if geo.blank?
66
+
67
+ "Nf=#{geo[:field]}|GCLT #{geo[:lat]},#{geo[:lng]} #{geo[:radius]/KM}"
68
+ end
69
+
70
+ # Nf - Range filter
71
+ # Sets a range filter with one of these formats:
72
+ # key|LT+number
73
+ # key|LTEQ+number
74
+ # key|GT+number
75
+ # key|GTEQ+number
76
+ # key|BTWN+num1+num2
77
+ # Nf=Price|LT+25
78
+ # Nf=Price|BTWN+8+15
79
+ # @params Array of hashes.
80
+ # Hash structure
81
+ # {
82
+ # :field => 'latitude',
83
+ # :values => ['30.123', '34.456'],
84
+ # :operator => 'BTWN'
85
+ # }
86
+ def range_filter(filters)
87
+ return nil if filters.blank?
88
+
89
+ filters.map! do |filter|
90
+ values = filter[:values].join(PLUS)
91
+ "#{filter[:field]}#{PIPE}#{filter[:operator]}#{PLUS}#{values}"
92
+ end
93
+
94
+ "Nf=#{filters.join(PIPE)}"
95
+ end
96
+
97
+ # Ne - indicating which dimension navigation refinements will be exposed
98
+ # @params - Array [123, 345, 678]
99
+ # Ne=123+345+678
100
+ def expand_refinements(refinements)
101
+ return nil if refinements.blank?
102
+
103
+ "Ne=#{refinements.join(PLUS)}"
104
+ end
105
+
106
+ # Ntk - Search filters key
107
+ # Ntt - Search filters term
108
+ # Ntk=showapartments|showcollege&Ntt=1|0
109
+ def where(where)
110
+ return nil if where.blank?
111
+
112
+ ntt = []
113
+ ntk = []
114
+
115
+ where.each_pair do |key, value|
116
+ ntk << key
117
+ ntt << value
118
+ end
119
+
120
+ "Ntk=#{ntk.join(PIPE)}&Ntt=#{ntt.join(PIPE)}"
121
+ end
122
+
123
+ # Nr - Record filters
124
+ # @params - Array: ['NOT(132831)', 'propertyB:valueY']
125
+ # Nr=AND(132831,propertyA:valueX,OR(propertyB:valueY,propertyC:valueZ))
126
+ # Nr=NOT(132831)&Nr=propertyB:valueY
127
+ def record_filters(filters)
128
+ return nil if filters.blank?
129
+
130
+ filters.collect! do |filter|
131
+ "Nr=#{filter}"
132
+ end.join('&')
133
+ end
134
+
135
+ # Nao - Aggregate records offset
136
+ # Nao=20
137
+ def aggregate_offset(offset)
138
+ offset.to_i == 0 ? nil : "Nao=#{offset}"
139
+ end
140
+
141
+ # No - Record offset
142
+ # No=20
143
+ def offset(offset)
144
+ offset.to_i == 0 ? nil : "No=#{offset}"
145
+ end
146
+
147
+ # M - Records per page
148
+ # @parms - number
149
+ # M=recs_per_page:10
150
+ def limit(limit)
151
+ limit.blank? ? nil : "M=recs_per_page:#{limit}"
152
+ end
153
+
154
+ # F - fields to selct
155
+ # @params - Array
156
+ # ['listingid', 'propertyname'] => 'F=listingid:1|propertyname:1'
157
+ def select(fields)
158
+ return nil if fields.blank?
159
+
160
+ field_list = []
161
+ fields.each do |field|
162
+ field_list << "#{field}:1" unless field.blank?
163
+ end
164
+
165
+ "F=#{field_list.join(PIPE)}"
166
+ end
167
+
168
+ # Nu - rollup records
169
+ # @params - String (listingid)
170
+ # Nu=listingid
171
+ def rollup(rolled_by)
172
+ return nil if rolled_by.blank?
173
+
174
+ "Nu=#{rolled_by}"
175
+ end
176
+
177
+ # Ntx - match mode
178
+ # @params - Array
179
+ # ['matchany', 'matchboolean'] => 'Ntx=mode matchany|mode matchboolean'
180
+ def match_mode(modes)
181
+ return nil if modes.blank?
182
+ mode_string = modes.map do |m|
183
+ "mode #{m}"
184
+ end.join(PIPE)
185
+
186
+ "Ntx=#{mode_string}"
187
+ end
188
+
189
+ end
190
+ end