four_tell 1.0.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0a2b99d95056ec90012bd1edbe1d3b19f98b2e5c
4
+ data.tar.gz: c50dc87fe437127f094cc8a6cda4197ce3d3851c
5
+ SHA512:
6
+ metadata.gz: 4ddd82c4cc00e2481f32cf37e855bbc83f6736b8d0021350c21e08245e53d2284682a34682bc022012a043d00f46078500d74a3e2707e2977a79d6fd6e51adb1
7
+ data.tar.gz: c1d2715dc0d74ce75851e19153d39cc014ae0a437e5b8545decac6cd036a0825c8c99e270ef4ea920cdff72656e70279a3fc83ef682b02a616f00a6a6041eab2
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ cache:
2
+ bundler: true
3
+ git:
4
+ depth: 1
5
+ language: ruby
6
+ rvm:
7
+ - 2.0.0
8
+ - 2.1.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in four_tell.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Jon-Michael Deldin
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # FourTell
2
+ [![Build Status](https://travis-ci.org/TheClymb/four_tell.png?branch=master)](https://travis-ci.org/TheClymb/four_tell)
3
+ [![Code Climate](https://codeclimate.com/github/TheClymb/four_tell.png)](https://codeclimate.com/github/TheClymb/four_tell)
4
+
5
+ Ruby API bindings for the [4-Tell](http://www.4-tell.com/)
6
+ personalization service.
7
+
8
+ ## Installation
9
+
10
+ FourTell supports Ruby 2.0+. Add this line to your application's
11
+ Gemfile:
12
+
13
+ gem 'four_tell'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install four_tell
22
+
23
+ ## Usage
24
+
25
+ See the
26
+ [documentation](http://rubydoc.info/github/TheClymb/four_tell/frames)
27
+ for more details.
28
+
29
+ ```ruby
30
+ require 'four_tell'
31
+ ft = FourTell.new('username', 'password', 'your_four_tell_client_alias')
32
+
33
+ # see FourTell::Request for more details
34
+ req = ft.build_request
35
+ req.customer_id = 1
36
+
37
+ # fetch a list of recommended product IDs
38
+ ft.recommendations(req)
39
+ ```
40
+
41
+ ## Contributing
42
+
43
+ 1. [Fork it](http://github.com/TheClymb/four_tell/fork)
44
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
45
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
46
+ 4. Push to the branch (`git push origin my-new-feature`)
47
+ 5. Create new Pull Request
48
+
49
+ ## Author
50
+
51
+ Built at [The Clymb](http://www.theclymb.com) by
52
+
53
+ - [Jon-Michael Deldin](https://github.com/jmdeldin)
54
+ - [Chris Kuttruff](https://github.com/ckuttruff)
55
+ - [Max Justus Spransy](https://github.com/maxjustus)
56
+
57
+ PS: [We're hiring!](http://www.theclymb.com/careers)
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << 'spec'
6
+ t.test_files = FileList['spec/*_spec.rb']
7
+ t.verbose = true
8
+ end
9
+
10
+ task :default => :test
data/four_tell.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'four_tell'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "four_tell"
8
+ spec.version = FourTell::VERSION
9
+ spec.authors = ["Jon-Michael Deldin"]
10
+ spec.email = ["dev@jmdeldin.com"]
11
+ spec.summary = "Ruby bindings to the 4-Tell API."
12
+ spec.homepage = "https://github.com/TheClymb/four_tell"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_development_dependency "bundler", "~> 1.5"
21
+ spec.add_development_dependency "rake"
22
+ spec.add_development_dependency "webmock"
23
+ spec.add_development_dependency "excon", "> 0.27"
24
+ spec.add_dependency 'net-http-persistent', '~> 2.9.4'
25
+ end
data/lib/four_tell.rb ADDED
@@ -0,0 +1,39 @@
1
+ require 'net/https'
2
+ require 'net/http/persistent'
3
+ require 'four_tell/request'
4
+ require 'json'
5
+
6
+ class FourTell
7
+ VERSION = "1.0.0"
8
+
9
+ # @param [String] api_user
10
+ # @param [String] api_password
11
+ # @param [String] client_alias
12
+ def initialize(api_user, api_password, client_alias)
13
+ @api_user = api_user
14
+ @api_password = api_password
15
+ @client_alias = client_alias
16
+ end
17
+
18
+ # Retrieve a list of recommended product IDs for a given request.
19
+ #
20
+ # @param [FourTell::Request] request
21
+ # @param [Fixnum] timeout Seconds to wait before timing out the request
22
+ # @raise [Timeout::Error, Errno::ECONNREFUSED, Errno::ECONNRESET]
23
+ # @return [Array]
24
+ def recommendations(request, timeout: 3)
25
+ url = URI(request.url)
26
+ response = Timeout::timeout(timeout) {
27
+ http = Net::HTTP::Persistent.new 'fourtell'
28
+ req = Net::HTTP::Get.new(url.request_uri)
29
+ req.basic_auth(@api_user, @api_password)
30
+ http.request(url, req)
31
+ }
32
+ (response && response.code == '200') ? JSON.parse(response.body) : []
33
+ end
34
+
35
+ # @return [FourTell::Request]
36
+ def build_request
37
+ FourTell::Request.new(@client_alias)
38
+ end
39
+ end
@@ -0,0 +1,141 @@
1
+ require 'set'
2
+
3
+ class FourTell
4
+ class Request
5
+ # @param [String] client_alias Your 4-Tell account name
6
+ def initialize(client_alias)
7
+ @client_alias = client_alias
8
+ @format = 'json'
9
+ @num_results = 20
10
+ @result_type = :cross_sell
11
+ end
12
+
13
+ # *REQUIRED*: The customer_id sent to 4-Tell. This should correspond to the
14
+ # customer ID sent via data exports.
15
+ #
16
+ # @param [Fixnum]
17
+ attr_writer :customer_id
18
+
19
+ # The result type to return
20
+ #
21
+ # - cross-sell: recommending something based on a customer's likes
22
+ # - personal: based upon purchase history
23
+ # - similar: just bought a shirt, here's another
24
+ #
25
+ # @param [Symbol]
26
+ attr_writer :result_type
27
+
28
+ # The number of desired results between 1-20 (20 is the max limit in the
29
+ # 4-Tell API)
30
+ #
31
+ # @param [Fixnum]
32
+ attr_writer :num_results
33
+
34
+ # The product ID of the primary item on the page
35
+ #
36
+ # @param [Fixnum]
37
+ attr_writer :product_id
38
+
39
+ # Ordered list of most recently visited to least recently visited product
40
+ # IDs. Only 5 unique product IDs will be included.
41
+ #
42
+ # @param [Array]
43
+ attr_writer :click_stream_product_ids
44
+
45
+ # List of product IDs in the customer's cart
46
+ #
47
+ # @param [Array]
48
+ attr_writer :cart_product_ids
49
+
50
+ # Help 4-Tell provide the right kind of recommendations for the page
51
+ #
52
+ # Can be one of:
53
+ # - Hm
54
+ # - Pdp1
55
+ # - Pdp2
56
+ # - Cat
57
+ # - Srch
58
+ # - Cart
59
+ # - Chkout
60
+ # - Bought
61
+ # - Admin
62
+ # - Other
63
+ #
64
+ # @param [Symbol]
65
+ attr_writer :page_type
66
+
67
+ # Parameters for the 4-Tell call. Empty parameters are omitted.
68
+ #
69
+ # @return [Hash]
70
+ def params
71
+ {}.tap { |h|
72
+ h['clientAlias'] = client_alias
73
+ h['format'] = format
74
+ h['customerID'] = customer_id
75
+ h['numResults'] = num_results
76
+ h['resultType'] = result_type
77
+ h['productIDs'] = product_id
78
+ h['clickStreamIDs'] = click_stream_product_ids
79
+ h['cartIDs'] = cart_product_ids
80
+ h['pageType'] = page_type
81
+ }.reduce({}) do |h, (k, v)|
82
+ h[k] = v if v
83
+ h
84
+ end
85
+ end
86
+
87
+ def url
88
+ u = URI('https://live.4-tell.net/Boost2.0/rest/GetRecIDs/array')
89
+ u.query = URI.encode_www_form(params)
90
+ u.to_s
91
+ end
92
+
93
+ private
94
+
95
+ RESULT_TYPES = {cross_sell: 0, personal: 1, similar: 2}
96
+ PAGE_TYPES = Set.new(%i(Hm Pdp1 Pdp2 Cat Srch Cart Chkout Bought Admin Other))
97
+
98
+ attr_reader :client_alias, :format, :product_id
99
+
100
+ def customer_id
101
+ @customer_id or raise(ArgumentError, 'customer_id is required')
102
+ end
103
+
104
+ def result_type
105
+ RESULT_TYPES.fetch(@result_type)
106
+ end
107
+
108
+ def num_results
109
+ n = Integer(@num_results)
110
+
111
+ if n <= 0 || n > 20
112
+ raise(ArgumentError, 'num_results must be between 1-20')
113
+ else
114
+ n
115
+ end
116
+ end
117
+
118
+ def click_stream_product_ids
119
+ unless empty_array?(@click_stream_product_ids)
120
+ @click_stream_product_ids.uniq.take(5).join(',')
121
+ end
122
+ end
123
+
124
+ def cart_product_ids
125
+ @cart_product_ids.uniq.join(',') unless empty_array?(@cart_product_ids)
126
+ end
127
+
128
+ def page_type
129
+ return nil unless @page_type
130
+
131
+ if @page_type && !PAGE_TYPES.include?(@page_type)
132
+ raise(ArgumentError, "#{@page_type} is an invalid page_type")
133
+ end
134
+ @page_type.to_s
135
+ end
136
+
137
+ def empty_array?(ary)
138
+ ary.nil? || ary.empty?
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+ require 'webmock/minitest'
3
+
4
+ describe FourTell do
5
+ let(:ft) { FourTell.new('username', 'password', 'client_alias') }
6
+
7
+ describe '#recommendations' do
8
+ it 'hits 4Tell' do
9
+ ids = (1..20).to_a.join(',')
10
+ base_url = "live.4-tell.net/Boost2.0/rest/GetRecIDs/array?clientAlias=client_alias&customerID=1&format=json&numResults=20&resultType=0"
11
+
12
+ stub_request(:get, "https://username:password@#{base_url}").
13
+ to_return(:status => 200, :body => "[#{ids}]")
14
+
15
+ req = MiniTest::Mock.new
16
+ req.expect :url, "https://#{base_url}"
17
+ ft.recommendations(req)
18
+
19
+ assert_requested(:get, "https://username:password@#{base_url}")
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,141 @@
1
+ require 'spec_helper'
2
+
3
+ describe FourTell::Request do
4
+ let(:req) {
5
+ FourTell::Request.new('the_client_alias').tap { |r| r.customer_id = 1 }
6
+ }
7
+
8
+ def fetch_param(key)
9
+ req.params.fetch(key.to_s)
10
+ end
11
+
12
+ describe '#client_alias' do
13
+ specify do
14
+ fetch_param(:clientAlias).must_equal 'the_client_alias'
15
+ end
16
+ end
17
+
18
+ describe '#num_results' do
19
+ it 'can be set' do
20
+ req.num_results = 3
21
+ fetch_param(:numResults).must_equal 3
22
+ end
23
+
24
+ it 'defaults to 20' do
25
+ fetch_param(:numResults).must_equal 20
26
+ end
27
+
28
+ let(:err_msg) { 'num_results must be between 1-20' }
29
+ it 'cannot be <= 0' do
30
+ err = assert_raises(ArgumentError) { req.num_results = 0; req.params }
31
+ assert_equal err_msg, err.message
32
+ end
33
+ it 'cannot be > 20' do
34
+ err = assert_raises(ArgumentError) { req.num_results = 21; req.params }
35
+ assert_equal err_msg, err.message
36
+ end
37
+ end
38
+
39
+ describe '#format' do
40
+ it 'is JSON' do
41
+ fetch_param(:format).must_equal 'json'
42
+ end
43
+ end
44
+
45
+ describe '#customer_id' do
46
+ it 'is included' do
47
+ req.customer_id = 100
48
+ fetch_param(:customerID).must_equal 100
49
+ end
50
+
51
+ it 'is required' do
52
+ r = FourTell::Request.new('the_client_alias')
53
+ err = assert_raises(ArgumentError) { r.params }
54
+ assert_equal 'customer_id is required', err.message
55
+ end
56
+ end
57
+
58
+ describe '#result_type' do
59
+ it 'defaults to cross-sell' do
60
+ fetch_param(:resultType).must_equal 0
61
+ end
62
+
63
+ it 'converts cross-sell' do
64
+ req.result_type = :cross_sell
65
+ fetch_param(:resultType).must_equal 0
66
+ end
67
+
68
+ it 'converts personal' do
69
+ req.result_type = :personal
70
+ fetch_param(:resultType).must_equal 1
71
+ end
72
+
73
+ it 'converts similar' do
74
+ req.result_type = :similar
75
+ fetch_param(:resultType).must_equal 2
76
+ end
77
+
78
+ it 'throws an error if given an unknown symbol' do
79
+ req.result_type = :magic
80
+ assert_raises(KeyError) { req.params }
81
+ end
82
+ end
83
+
84
+ describe '#product_id' do
85
+ it 'is included' do
86
+ req.product_id = 314159
87
+ fetch_param(:productIDs).must_equal 314159
88
+ end
89
+ end
90
+
91
+ describe '#click_stream_product_ids' do
92
+ it 'sets these to clickStreamIDs' do
93
+ req.click_stream_product_ids = [3, 1, 4]
94
+ fetch_param(:clickStreamIDs).must_equal '3,1,4'
95
+ end
96
+
97
+ it 'dedupes and limits to 5' do
98
+ req.click_stream_product_ids = [3, 1, 4, 1, 5, 9, 2]
99
+ fetch_param(:clickStreamIDs).must_equal '3,1,4,5,9'
100
+ end
101
+ end
102
+
103
+ describe '#cart_product_ids' do
104
+ it 'dedupes' do
105
+ req.cart_product_ids = [3, 1, 4, 1, 5, 9, 2]
106
+ fetch_param(:cartIDs).must_equal '3,1,4,5,9,2'
107
+ end
108
+ end
109
+
110
+ describe '#page_type' do
111
+ it 'validates' do
112
+ %i(Hm Pdp1 Pdp2 Cat Srch Cart Chkout Bought Admin Other).each do |t|
113
+ req.page_type = t
114
+ fetch_param(:pageType).must_equal t.to_s
115
+ end
116
+ end
117
+
118
+ it 'can be nil' do
119
+ req.page_type = nil
120
+ req.params['pageType'].must_be_nil
121
+ end
122
+
123
+ it 'raises an error on invalid page_types' do
124
+ req.page_type = 'SECRET HIDEOUT'
125
+ err = assert_raises(ArgumentError) { fetch_param(:pageType) }
126
+ assert_equal 'SECRET HIDEOUT is an invalid page_type', err.message
127
+ end
128
+ end
129
+
130
+ describe '#url' do
131
+ let(:base_url) {
132
+ 'https://live.4-tell.net/Boost2.0/rest/GetRecIDs/array?clientAlias=the_client_alias&format=json'
133
+ }
134
+
135
+ it 'omits empty parameters' do
136
+ req.product_id = 2
137
+ u = base_url + '&customerID=1&numResults=20&resultType=0&productIDs=2'
138
+ req.url.must_equal u
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,3 @@
1
+ require 'minitest/autorun'
2
+ require 'minitest/pride'
3
+ require 'four_tell'
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: four_tell
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jon-Michael Deldin
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-05-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.5'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: webmock
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: excon
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.27'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.27'
69
+ - !ruby/object:Gem::Dependency
70
+ name: net-http-persistent
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 2.9.4
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 2.9.4
83
+ description:
84
+ email:
85
+ - dev@jmdeldin.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".travis.yml"
92
+ - Gemfile
93
+ - LICENSE.txt
94
+ - README.md
95
+ - Rakefile
96
+ - four_tell.gemspec
97
+ - lib/four_tell.rb
98
+ - lib/four_tell/request.rb
99
+ - spec/four_tell_spec.rb
100
+ - spec/request_spec.rb
101
+ - spec/spec_helper.rb
102
+ homepage: https://github.com/TheClymb/four_tell
103
+ licenses:
104
+ - MIT
105
+ metadata: {}
106
+ post_install_message:
107
+ rdoc_options: []
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ requirements: []
121
+ rubyforge_project:
122
+ rubygems_version: 2.2.2
123
+ signing_key:
124
+ specification_version: 4
125
+ summary: Ruby bindings to the 4-Tell API.
126
+ test_files:
127
+ - spec/four_tell_spec.rb
128
+ - spec/request_spec.rb
129
+ - spec/spec_helper.rb
130
+ has_rdoc: