awesome_o 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 30a48a2ae38fffe8accddd03c162793b902a9001
4
+ data.tar.gz: 102caa5f774e27de2f0c5d220d60f2071a677e47
5
+ SHA512:
6
+ metadata.gz: aaf745d0d79bf6184e792919b663e8fdf575ab5c892f037994fe2f4e443e30697ec3be05da2c9f6257afe112e30dc0ee7bfa939062d741d3824cdb201fbe44f9
7
+ data.tar.gz: f8b853877ad3d3d5ddd0e7e852c7ccf9d9e16844c9cdf541085140c0810ca58a97f7ecccdecec8a0d6a521e3ffe8e27c8d29e177510d61e59684fe37f6281060
@@ -0,0 +1,20 @@
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
18
+ *.swp
19
+ *.un~
20
+ *.rdb
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - "1.9.2"
4
+ - "1.9.3"
5
+ - "2.0.0"
6
+ services:
7
+ - redis
8
+ script: bundle exec rspec spec
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in awesome_o.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Will Cosgrove
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.
@@ -0,0 +1,114 @@
1
+ # AwesomeO
2
+ [![Build Status](https://travis-ci.org/hadees/awesome_o.svg?branch=master)](https://travis-ci.org/hadees/awesome_o)
3
+
4
+ ![](http://i.imgur.com/rziNSZZ.png)
5
+
6
+ AwesomeO is a framework agnostic, redis backed, cart system. It is not a POS, or a full fledged ecommerce system. Just the cart, man.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ gem 'awesome_o'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install awesome_o
21
+
22
+ ## Setup
23
+
24
+ AwesomeO has a few (read 3) configuration options you can set, most likely in an initializer file. Here's an example configuration:
25
+
26
+ ```ruby
27
+ # config/initializers/awesome_o.rb
28
+ AwesomeO.config do |c|
29
+ c.cart_expires_in = 604800 # one week, in seconds. This is the default
30
+ c.unit_cost_field = :unit_cost # for cart totaling
31
+ c.quantity_field = :quantity # for quantity totaling
32
+ c.redis = Redis.new # set the redis connection here
33
+ end
34
+ ```
35
+
36
+ - The `cart_expires_in=` setting will let you set how long a cart should live. If no items are added to the cart before the time expires, the cart will be cleared. If you want to disable cart expiration, set this to `-1`.
37
+ - `unit_cost_field=` lets you tell AwesomeO where you're storing the unit_cost of each item, so that you can use the `cost` method on the item, and the `total` method on `Cart`.
38
+ - `quantity_field=` lets you tell AwesomeO where you're storing the quantity of each item. The `Item#cost` method uses this along with the `unit_cost` field to determine the cost.
39
+ - `redis=` lets you set the redis connection you want to use. Note that this is not redis connection options, this is an actual instance of `Redis`.
40
+
41
+ ## Usage
42
+
43
+ To create a new shopping cart, just call `AwesomeO::Cart.new(user.id)`. The parameter for `Cart.new()` is a unique id. If you don't want a user to have more than one cart at a time, it's generally best to set this to the user's id. Then to add an item to the cart, just call `cart.add_item(data)` which takes a hash of data that you want to store. Then to retrieve the items, you just call `cart.items` which will give you an array of all the items they've added.
44
+
45
+ The returned Items come back as `AwesomeO::Item` instances, which have a few special methods to be aware of:
46
+
47
+ - `cost` - which will return the `:unit_cost` multiplied by the `:quantity`.
48
+ - `destroy` - which will remove the item from the cart
49
+ - `cart` - which will return the parent cart, think ActiveRecord association
50
+ - `_id` - which will return the id of the item, if you need that for whatever reason
51
+ - `_key` - which will return the redis key the data is stored in. Probably won't need that, but it's there.
52
+ - `#{attribute}=` - this is a setter defined for all of the items attributes that you gave it. It will instantly save to redis also, so no need to call `save` (which is why there isn't a `save`).
53
+
54
+ The `Cart` object also has some handy methods that you should be aware of:
55
+
56
+ - `add_item(data)` - which is the life blood of AwesomeO. This method takes a hash of data you would like to store with the item. Here's a few suggestions of keys you may want in your hash:
57
+ - `:id` - (*required*) to store the ID of the model you're adding
58
+ - `:type` - (*required*) to store the class of the model you're adding.
59
+ - `:unit_cost` - This field is required if you want to take advantage of the `Item#cost` method, and the `Cart#total` method.
60
+ - `:quantity` - which if you use will let you use the `Cart#quantity` and `Cart#total` methods without any extra configuration.
61
+ - `remove_item(item)` - which, you guessed it, removes an item. This method takes an Item object, not a hash.
62
+ - `contains?(Product)` - This is a biggie. It will tell you if a certain item is in the cart. And the way it works is you pass it an object, like an instance of a Product model, and it will examine the class, and the id, and look to see if it's in the cart already. This method only works if the `:id` and `:type` keys are set in the item's data hash.
63
+ - `find(Product)` - This will return the `Item` object that represents the object passed in. It works like `contains?` and uses class, and id. It only works if the `:id` and `:type` keys are set in the item's data hash.
64
+ - `items` - this returns a magic array of all the items. You can also pass in a type string, which will return a magic array of all the items with a matching type. I call it magic because you can call on it:
65
+ - `each_with_object` - which will act like a regular `each` call, but the block will yield the `Item` and the object it represents by using the `:type` and `:id` keys.
66
+ - `total` - will sum all of the `cost` return values of all of the items in the cart. For this to work, the `:unit_cost` and `:quantity` fields need to be set for all items.
67
+ - `count` - which will give you the total number of items in the cart. Faster than `cart.items.size` because it doesn't load all of the item data from redis.
68
+ - `quantity` - which will return the total quantity of all the items. The quantity field is set in the config block, by default it's `:quantity`
69
+ - `ttl` - will tell you how many seconds until the cart expires. It will return -1 if the cart will never expire.
70
+ - `touch` - which will reset the ttl back to whatever expiration length is set in the config. Touch is automatically called after `add_item` and `remove_item`.
71
+ - `destroy!` - which will delete the cart, and all the line_items out of it.
72
+ - `reassign(id)` - this method will reassign the cart's unique identifier. So to access this cart at a later time after reassigning, you would put `Cart.new(reassigned_id)`. This is useful for having a cart for an unsigned in user. You can use their session ID while they're unauthenticated, and then when they sign in, you can reassign the cart to the user's ID. NOTE: Reassigning will overwrite any cart in it's way.
73
+
74
+ Lets walk through an example implementation with a Rails app that has a User model and a Product model.
75
+
76
+ ```ruby
77
+ # app/models/user.rb
78
+ class User < ActiveRecord::Base
79
+ #...
80
+ def cart
81
+ AwesomeO::Cart.new(id)
82
+ end
83
+ #...
84
+ end
85
+ ```
86
+
87
+ ```ruby
88
+ # app/controllers/products_controller.rb
89
+ class ProductsController < ApplicationController
90
+ #...
91
+ # /products/:id/add_to_cart
92
+ def add_to_cart
93
+ @product = Product.find(params[:id])
94
+ current_user.cart.add_item(id: @product.id, name: @product.name, unit_cost: @product.cost, cost: @product.cost * params[:quantity], quantity: params[:quantity])
95
+ end
96
+ #...
97
+ end
98
+ ```
99
+
100
+ ```haml
101
+ -# app/view/cart/show.html.haml
102
+ %h1 Cart - Total: #{@cart.total}
103
+ %ul
104
+ - @cart.items.each do |item|
105
+ %li #{item.name} - #{item.cost}
106
+ ```
107
+
108
+ ## Contributing
109
+
110
+ 1. Fork it
111
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
112
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
113
+ 4. Push to the branch (`git push origin my-new-feature`)
114
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'awesome_o/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = 'awesome_o'
8
+ gem.version = AwesomeO::VERSION
9
+ gem.authors = ['Evan Alter']
10
+ gem.email = ['evan.alter@gmail.com']
11
+ gem.description = 'AwesomeO is a frameworke agnostic, redis-backed, shopping cart system'
12
+ gem.summary = 'Originaly forked from the gem Cartman'
13
+ gem.homepage = 'http://github.com/hadees/awesome-o'
14
+ gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.require_paths = ['lib']
18
+
19
+ gem.add_dependency('redis')
20
+
21
+ gem.add_development_dependency('rspec', '~> 3.0.0')
22
+ end
@@ -0,0 +1,13 @@
1
+ require 'awesome_o/version'
2
+ require 'awesome_o/configuration'
3
+ require 'awesome_o/cart'
4
+ require 'awesome_o/item'
5
+ require 'awesome_o/item_collection'
6
+
7
+ module AwesomeO
8
+ def self.config
9
+ @config ||= Configuration.new
10
+ yield @config if block_given?
11
+ @config
12
+ end
13
+ end
@@ -0,0 +1,170 @@
1
+ module AwesomeO
2
+ class Cart
3
+ CART_LINE_ITEM_ID_KEY = 'awesome_o:line_item:id'.freeze
4
+
5
+ def initialize(uid)
6
+ @uid = uid
7
+ end
8
+
9
+ def add_item(options)
10
+ raise 'Must specify both :id and :type' unless options.key?(:id) && options.key?(:type)
11
+ line_item_id = redis.incr CART_LINE_ITEM_ID_KEY
12
+ redis.pipelined do
13
+ redis.mapped_hmset("awesome_o:line_item:#{line_item_id}", options)
14
+ redis.hincrby("awesome_o:line_item:#{line_item_id}", :_version, 1)
15
+ redis.sadd key, line_item_id
16
+ redis.sadd index_key, "#{options[:type]}:#{options[:id]}"
17
+ redis.set index_key_for(options), line_item_id
18
+ end
19
+ touch
20
+ get_item(line_item_id)
21
+ end
22
+
23
+ def remove_item(item)
24
+ redis.del "awesome_o:line_item:#{item._id}"
25
+ redis.srem key, item._id
26
+ redis.srem index_key, "#{item.type}:#{item.id}"
27
+ redis.del index_key_for(item)
28
+ touch
29
+ end
30
+
31
+ def items(type = nil)
32
+ if type
33
+ items = line_item_ids.collect { |item_id| get_item(item_id) }.select { |item| item.type == type }
34
+ return ItemCollection.new(items)
35
+ else
36
+ return ItemCollection.new(line_item_ids.collect { |item_id| get_item(item_id) })
37
+ end
38
+ end
39
+
40
+ def contains?(object)
41
+ redis.sismember index_key, "#{object.class}:#{object.id}"
42
+ end
43
+
44
+ def find(object)
45
+ get_item(redis.get(index_key_for(object)).to_i) if contains?(object)
46
+ end
47
+
48
+ def count
49
+ redis.scard key
50
+ end
51
+
52
+ def quantity
53
+ line_item_keys.collect do |item_key|
54
+ redis.hget item_key, AwesomeO.config.quantity_field
55
+ end.inject(0) { |sum, quantity| sum += quantity.to_i }
56
+ end
57
+
58
+ def total
59
+ items.collect do |item|
60
+ (item.cost * 100).to_i
61
+ end.inject(0) { |sum, cost| sum += cost } / 100.0
62
+ end
63
+
64
+ def ttl
65
+ redis.ttl key
66
+ end
67
+
68
+ def destroy!
69
+ keys = line_item_keys
70
+ keys << key
71
+ keys << index_key
72
+ keys << index_keys
73
+ keys.flatten!
74
+ redis.pipelined do
75
+ keys.each do |key|
76
+ redis.del key
77
+ end
78
+ end
79
+ end
80
+
81
+ def touch
82
+ keys_to_expire = line_item_keys
83
+ keys_to_expire << key
84
+ if redis.exists index_key
85
+ keys_to_expire << index_key
86
+ keys_to_expire << index_keys
87
+ keys_to_expire.flatten!
88
+ end
89
+ redis.pipelined do
90
+ keys_to_expire.each do |item|
91
+ redis.expire item, AwesomeO.config.cart_expires_in
92
+ end
93
+ end
94
+ redis.hincrby self.class.versions_key, version_key, 1
95
+ end
96
+
97
+ def version
98
+ redis.hget(self.class.versions_key, version_key).to_i
99
+ end
100
+
101
+ def reassign(new_id)
102
+ if redis.exists key
103
+ new_index_keys = items.collect do |item|
104
+ index_key_for(item, new_id)
105
+ end
106
+ redis.rename key, key(new_id)
107
+ redis.rename index_key, index_key(new_id)
108
+ index_keys.zip(new_index_keys).each do |key, value|
109
+ redis.rename key, value
110
+ end
111
+ end
112
+ @uid = new_id
113
+ end
114
+
115
+ def cache_key
116
+ "cart/#{@uid}-#{version}"
117
+ end
118
+
119
+ private
120
+
121
+ def key(id = @uid)
122
+ "awesome_o:cart:#{id}"
123
+ end
124
+
125
+ def index_key(id = @uid)
126
+ key(id) + ':index'
127
+ end
128
+
129
+ def version_key(id = @uid)
130
+ id
131
+ end
132
+
133
+ def self.versions_key
134
+ 'awesome_o:cart:versions'
135
+ end
136
+
137
+ def index_keys(id = @uid)
138
+ redis.keys "#{index_key(id)}:*"
139
+ end
140
+
141
+ def index_key_for(object, id = @uid)
142
+ case object
143
+ when Hash
144
+ index_key(id) + ":#{object[:type]}:#{object[:id]}"
145
+ when Item
146
+ index_key(id) + ":#{object.type}:#{object.id}"
147
+ else
148
+ index_key(id) + ":#{object.class}:#{object.id}"
149
+ end
150
+ end
151
+
152
+ def line_item_ids
153
+ redis.smembers key
154
+ end
155
+
156
+ def line_item_keys
157
+ line_item_ids.collect { |id| "awesome_o:line_item:#{id}" }
158
+ end
159
+
160
+ def get_item(id)
161
+ Item.new(id, @uid, redis.hgetall("awesome_o:line_item:#{id}").each_with_object({}) { |(k, v), hash| hash[k.to_sym] = v; hash })
162
+ end
163
+
164
+ private
165
+
166
+ def redis
167
+ AwesomeO.config.redis
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,18 @@
1
+ require 'redis'
2
+
3
+ module AwesomeO
4
+ class Configuration
5
+ attr_accessor :cart_expires_in, :unit_cost_field, :quantity_field
6
+ attr_writer :redis
7
+
8
+ def initialize
9
+ @cart_expires_in = 604_800
10
+ @unit_cost_field = :unit_cost
11
+ @quantity_field = :quantity
12
+ end
13
+
14
+ def redis
15
+ @redis ||= Redis.new
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,71 @@
1
+ module AwesomeO
2
+ class Item
3
+ def initialize(item_id, cart_id, data)
4
+ @id = item_id
5
+ @cart_id = cart_id
6
+ @data = data
7
+ end
8
+
9
+ def _id
10
+ @id
11
+ end
12
+
13
+ def cost
14
+ unit_cost = (@data.fetch(AwesomeO.config.unit_cost_field).to_f * 100).to_i
15
+ quantity = @data.fetch(AwesomeO.config.quantity_field).to_i
16
+ (unit_cost * quantity) / 100.0
17
+ end
18
+
19
+ def cart
20
+ @cart ||= Cart.new(@cart_id)
21
+ end
22
+
23
+ def destroy
24
+ cart.remove_item(self)
25
+ end
26
+
27
+ def ==(item)
28
+ @id == item._id
29
+ end
30
+
31
+ def touch
32
+ cart.touch
33
+ redis.hincrby _key, :_version, 1
34
+ end
35
+
36
+ def _key
37
+ "awesome_o:line_item:#{@id}"
38
+ end
39
+
40
+ def _version
41
+ super.to_i
42
+ end
43
+
44
+ def cache_key
45
+ "item/#{@id}-#{_version}"
46
+ end
47
+
48
+ def method_missing(method, *args, &block)
49
+ if method.to_s.end_with?('=')
50
+ redis.hset _key, method[0..-2], args[0].to_s
51
+ @data.store(method[0..-2].to_sym, args[0].to_s)
52
+ version = touch
53
+ @data.store(:_version, version)
54
+ elsif @data.keys.include?(method)
55
+ @data.fetch(method)
56
+ else
57
+ super
58
+ end
59
+ end
60
+
61
+ def respond_to_missing?(method, include_private = false)
62
+ method.to_s.end_with?('=') || @data.keys.include?(method) || super
63
+ end
64
+
65
+ private
66
+
67
+ def redis
68
+ AwesomeO.config.redis
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,18 @@
1
+ require 'delegate'
2
+
3
+ module AwesomeO
4
+ class ItemCollection < DelegateClass(Array)
5
+ include Enumerable
6
+
7
+ def initialize(array)
8
+ @results = array
9
+ super(@results)
10
+ end
11
+
12
+ def each_with_object
13
+ @results.each do |result|
14
+ yield result, eval(result.type).send(:find, result.id)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ module AwesomeO
2
+ VERSION = '0.0.1'.freeze
3
+ end
@@ -0,0 +1,259 @@
1
+ require 'spec_helper'
2
+
3
+ describe AwesomeO do
4
+ describe AwesomeO::Cart do
5
+ let(:Bottle) { Struct.new(:id) }
6
+ let(:cart) { AwesomeO::Cart.new(1) }
7
+
8
+ before(:each) do
9
+ AwesomeO.config.redis.flushdb
10
+ end
11
+
12
+ describe '#key' do
13
+ it 'should return a proper key string' do
14
+ expect(cart.send(:key)).to eq('awesome_o:cart:1')
15
+ end
16
+ end
17
+
18
+ describe '#add_item' do
19
+ before(:each) do
20
+ cart.add_item(id: 17, type: 'Bottle', name: 'Bordeux', unit_cost: 92.12, quantity: 2)
21
+ end
22
+
23
+ it 'creates a line item key' do
24
+ expect(AwesomeO.config.redis.exists('awesome_o:line_item:1')).to be true
25
+ end
26
+
27
+ it "adds that line item key's id to the cart set" do
28
+ expect(AwesomeO.config.redis.sismember(cart.send(:key), 1)).to be true
29
+ end
30
+
31
+ it 'should expire the line_item_keys in the amount of time specified' do
32
+ expect(cart.ttl).to eq(AwesomeO.config.cart_expires_in)
33
+ expect(AwesomeO.config.redis.ttl('awesome_o:line_item:1')).to eq(AwesomeO.config.cart_expires_in)
34
+ end
35
+
36
+ it 'should add an index key to be able to look up by type and ID' do
37
+ expect(AwesomeO.config.redis.exists('awesome_o:cart:1:index')).to be true
38
+ expect(AwesomeO.config.redis.sismember('awesome_o:cart:1:index', 'Bottle:17')).to be true
39
+ end
40
+
41
+ it 'should squack if type and/or ID are not set' do
42
+ expect { cart.add_item(id: 18, name: 'Cordeux', unit_cost: 92.12, quantity: 2) }.to raise_error('Must specify both :id and :type')
43
+ expect { cart.add_item(type: 'Bottle', name: 'Cordeux', unit_cost: 92.12, quantity: 2) }.to raise_error('Must specify both :id and :type')
44
+ expect { cart.add_item(name: 'Cordeux', unit_cost: 92.12, quantity: 2) }.to raise_error('Must specify both :id and :type')
45
+ end
46
+
47
+ it 'should return an Item' do
48
+ item = cart.add_item(id: 34, type: 'Bottle', name: 'Cabernet', unit_cost: 92.12, quantity: 2)
49
+ expect(item.class).to eq(AwesomeO::Item)
50
+ end
51
+ end
52
+
53
+ describe '#remove_item' do
54
+ it 'should remove the id from the set, and delete the line_item key' do
55
+ item = cart.add_item(id: 17, type: 'Bottle', name: 'Bordeux', unit_cost: 92.12, quantity: 2)
56
+ item_id = item._id
57
+ cart.remove_item(item)
58
+ expect(AwesomeO.config.redis.sismember(cart.send(:key), item_id)).to be false
59
+ expect(AwesomeO.config.redis.exists("awesome_o:line_item:#{item_id}")).to be false
60
+ end
61
+
62
+ it 'should not delete the indecies for other items' do
63
+ item = cart.add_item(id: 17, type: 'Bottle', name: 'Bordeux', unit_cost: 92.12, quantity: 2)
64
+ item2 = cart.add_item(id: 18, type: 'Bottle', name: 'Bordeux', unit_cost: 92.12, quantity: 2)
65
+ expect(AwesomeO.config.redis.exists('awesome_o:cart:1:index:Bottle:17')).to be true
66
+ expect(AwesomeO.config.redis.exists('awesome_o:cart:1:index:Bottle:18')).to be true
67
+ cart.remove_item(item)
68
+ expect(AwesomeO.config.redis.exists('awesome_o:cart:1:index:Bottle:17')).to be false
69
+ expect(AwesomeO.config.redis.exists('awesome_o:cart:1:index:Bottle:18')).to be true
70
+ end
71
+ end
72
+
73
+ describe '#items' do
74
+ before(:each) do
75
+ cart.add_item(id: 17, type: 'Bottle', name: 'Bordeux', unit_cost: 92.12, quantity: 2)
76
+ cart.add_item(id: 34, type: 'Bottle', name: 'Cabernet', unit_cost: 92.12, quantity: 2)
77
+ cart.add_item(id: 35, type: 'GiftCard', name: 'Gift Card', unit_cost: 100.00, quantity: 1)
78
+ end
79
+
80
+ it 'should return an ItemCollection of Items' do
81
+ expect(cart.items.class).to be(AwesomeO::ItemCollection)
82
+ expect(cart.items.first.class).to be(AwesomeO::Item)
83
+ expect(cart.items.first.id).to eq('17')
84
+ expect(cart.items.first.name).to eq('Bordeux')
85
+ end
86
+
87
+ it 'should return all items in cart if no filter is given' do
88
+ expect(cart.items.size).to eq(3)
89
+ end
90
+
91
+ it 'should return a subset of the items if a filter is given' do
92
+ expect(cart.items('Bottle').size).to eq(2)
93
+ expect(cart.items('GiftCard').size).to eq(1)
94
+ expect(cart.items('SomethingElse').size).to eq(0)
95
+ end
96
+ end
97
+
98
+ describe '#contains?(item)' do
99
+ before(:all) do
100
+ Bottle = Struct.new(:id)
101
+ end
102
+
103
+ before(:each) do
104
+ cart.add_item(id: 17, type: 'Bottle', name: 'Bordeux', unit_cost: 92.12, quantity: 2)
105
+ cart.add_item(id: 34, type: 'Bottle', name: 'Cabernet', unit_cost: 92.12, quantity: 2)
106
+ end
107
+
108
+ it 'should be able to tell you that an item in the cart is present' do
109
+ expect(cart.contains?(Bottle.new(17))).to be true
110
+ end
111
+
112
+ it 'should be able to tell you that an item in the cart is absent' do
113
+ expect(cart.contains?(Bottle.new(20))).to be false
114
+ end
115
+
116
+ it "should be able to tell you that an item in the cart is absent if it's been removed" do
117
+ cart.remove_item(cart.items.first)
118
+ expect(cart.contains?(Bottle.new(17))).to be false
119
+ cart.remove_item(cart.items.last)
120
+ expect(cart.contains?(Bottle.new(34))).to be false
121
+ end
122
+ end
123
+
124
+ describe '#find(item)' do
125
+ before(:each) do
126
+ cart.add_item(id: 17, type: 'Bottle', name: 'Bordeux', unit_cost: 92.12, quantity: 2)
127
+ cart.add_item(id: 34, type: 'Bottle', name: 'Cabernet', unit_cost: 92.12, quantity: 2)
128
+ end
129
+
130
+ it 'should take some object, and return the Item that corresponds to it' do
131
+ expect(cart.find(Bottle.new(17)).quantity).to eq('2')
132
+ expect(cart.find(Bottle.new(17)).name).to eq('Bordeux')
133
+ expect(cart.find(Bottle.new(34)).name).to eq('Cabernet')
134
+ end
135
+
136
+ it 'should return nil if the Item is not in the cart' do
137
+ expect(cart.find(Bottle.new(23))).to be(nil)
138
+ end
139
+ end
140
+
141
+ describe '#count' do
142
+ it 'should return the number of items in the cart' do
143
+ cart.add_item(id: 17, type: 'Bottle', name: 'Bordeux', unit_cost: 92.12, quantity: 2)
144
+ cart.add_item(id: 34, type: 'Bottle', name: 'Cabernet', unit_cost: 92.12, quantity: 2)
145
+ expect(cart.count).to eq(2)
146
+ end
147
+ end
148
+
149
+ describe '#quantity' do
150
+ it 'should return the sum of the default quantity field' do
151
+ cart.add_item(id: 17, type: 'Bottle', name: 'Bordeux', unit_cost: 92.12, quantity: 2)
152
+ cart.add_item(id: 34, type: 'Bottle', name: 'Cabernet', unit_cost: 92.12, quantity: 2)
153
+ expect(cart.quantity).to eq(4)
154
+ end
155
+
156
+ it 'should return the sum of the defined quantity field' do
157
+ AwesomeO.config do |c|
158
+ c.quantity_field = :qty
159
+ end
160
+ cart.add_item(id: 17, type: 'Bottle', name: 'Bordeux', unit_cost: 92.12, qty: 2)
161
+ cart.add_item(id: 34, type: 'Bottle', name: 'Cabernet', unit_cost: 92.12, qty: 2)
162
+ expect(cart.quantity).to eq(4)
163
+ AwesomeO.config do |c|
164
+ c.quantity_field = :quantity
165
+ end
166
+ end
167
+ end
168
+
169
+ describe '#total' do
170
+ it 'should return 0 when no items are in the cart' do
171
+ expect(cart.total).to eq(0)
172
+ end
173
+
174
+ it 'should total the default costs field' do
175
+ cart.add_item(id: 17, type: 'Bottle', name: 'Bordeux', unit_cost: 92.12, quantity: 2)
176
+ cart.add_item(id: 34, type: 'Bottle', name: 'Cabernet', unit_cost: 92.12, quantity: 2)
177
+ expect(cart.total).to eq(368.48)
178
+ end
179
+
180
+ it 'should total whatever cost field the user sets' do
181
+ AwesomeO.config do |c|
182
+ c.unit_cost_field = :unit_cost_in_cents
183
+ end
184
+ cart.add_item(id: 17, type: 'Bottle', name: 'Bordeux', unit_cost_in_cents: 9212, quantity: 2)
185
+ cart.add_item(id: 34, type: 'Bottle', name: 'Cabernet', unit_cost_in_cents: 9212, quantity: 2)
186
+ expect(cart.total).to eq(36_848)
187
+ AwesomeO.config do |c|
188
+ c.unit_cost_field = :unit_cost
189
+ end
190
+ end
191
+ end
192
+
193
+ describe '#destroy' do
194
+ it 'should delete the line_item keys, the index key, and the cart key' do
195
+ cart.add_item(id: 17, type: 'Bottle', name: 'Bordeux', unit_cost: 92.12, cost_in_cents: 18_424, quantity: 2)
196
+ cart.add_item(id: 34, type: 'Bottle', name: 'Cabernet', unit_cost: 92.12, cost_in_cents: 18_424, quantity: 2)
197
+ cart.destroy!
198
+ expect(AwesomeO.config.redis.exists('awesome_o:cart:1')).to be false
199
+ expect(AwesomeO.config.redis.exists('awesome_o:line_item:1')).to be false
200
+ expect(AwesomeO.config.redis.exists('awesome_o:line_item:2')).to be false
201
+ expect(AwesomeO.config.redis.exists('awesome_o:cart:1:index')).to be false
202
+ expect(AwesomeO.config.redis.exists('awesome_o:cart:1:index:Bottle:17')).to be false
203
+ end
204
+ end
205
+
206
+ describe '#touch' do
207
+ it 'should reset the TTL' do
208
+ cart.add_item(id: 17, type: 'Bottle', name: 'Bordeux', unit_cost: 92.12, cost_in_cents: 18_424, quantity: 2)
209
+ cart.touch
210
+ expect(cart.ttl).to eq(AwesomeO.config.cart_expires_in)
211
+ expect(AwesomeO.config.redis.ttl('awesome_o:cart:1:index')).to eq(AwesomeO.config.cart_expires_in)
212
+ expect(AwesomeO.config.redis.ttl('awesome_o:cart:1:index:Bottle:17')).to eq(AwesomeO.config.cart_expires_in)
213
+ end
214
+
215
+ it 'should record that the cart was updated' do
216
+ cart.add_item(id: 17, type: 'Bottle', name: 'Bordeux', unit_cost: 92.12, cost_in_cents: 18_424, quantity: 2)
217
+ cart.touch
218
+ expect(cart.version).to eq(2)
219
+ end
220
+ end
221
+
222
+ describe '#reassign' do
223
+ it 'should only change the @uid if no key exists' do
224
+ cart.reassign(2)
225
+ expect(cart.send(:key)[-1]).to eq('2')
226
+ end
227
+
228
+ it 'should rename the key, and index_key if it exists' do
229
+ cart.add_item(id: 17, type: 'Bottle', name: 'Bordeux', unit_cost: 92.12, cost_in_cents: 18_424, quantity: 1)
230
+ cart.add_item(id: 18, type: 'Bottle', name: 'Merlot', unit_cost: 92.12, cost_in_cents: 18_424, quantity: 3)
231
+ expect(cart.quantity).to be(4)
232
+ cart.reassign(2)
233
+ expect(cart.items.size).to be(2)
234
+ expect(AwesomeO::Cart.new(2).quantity).to be(4)
235
+ expect(AwesomeO.config.redis.exists('awesome_o:cart:1')).to be false
236
+ expect(AwesomeO.config.redis.exists('awesome_o:cart:1:index')).to be false
237
+ expect(AwesomeO.config.redis.exists('awesome_o:cart:1:index:Bottle:17')).to be false
238
+ expect(AwesomeO.config.redis.exists('awesome_o:cart:2')).to be true
239
+ expect(AwesomeO.config.redis.exists('awesome_o:cart:2:index')).to be true
240
+ expect(AwesomeO.config.redis.exists('awesome_o:cart:2:index:Bottle:17')).to be true
241
+ expect(cart.send(:key)[-1]).to eq('2')
242
+ cart.add_item(id: 19, type: 'Bottle', name: 'Bordeux', unit_cost: 92.12, cost_in_cents: 18_424, quantity: 2)
243
+ cart.reassign(1)
244
+ expect(cart.items.size).to be(3)
245
+ expect(AwesomeO.config.redis.exists('awesome_o:cart:2')).to be false
246
+ expect(AwesomeO.config.redis.exists('awesome_o:cart:2:index')).to be false
247
+ expect(AwesomeO.config.redis.exists('awesome_o:cart:1')).to be true
248
+ expect(AwesomeO.config.redis.exists('awesome_o:cart:1:index')).to be true
249
+ expect(cart.send(:key)[-1]).to eq('1')
250
+ end
251
+ end
252
+
253
+ describe '#cache_key' do
254
+ it 'should return /cart/{cart_id}-{version}/' do
255
+ expect(cart.cache_key).to eq("cart/#{cart.instance_variable_get(:@uid)}-#{cart.version}")
256
+ end
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ describe AwesomeO do
4
+ describe AwesomeO::ItemCollection do
5
+ let(:cart) { AwesomeO::Cart.new(1) }
6
+
7
+ before(:each) do
8
+ AwesomeO.config.redis.flushdb
9
+ Object.send(:remove_const, :Bottle) if defined?(Bottle)
10
+ Bottle = double
11
+ allow(Bottle).to receive(:find).and_return(bottle)
12
+ end
13
+
14
+ describe '#each_with_object' do
15
+ let(:bottle) { double('Bottle') }
16
+
17
+ it 'should be magical' do
18
+ cart.add_item(id: 17, type: 'Bottle', name: 'Bordeux', unit_cost: 92.12, cost: 184.24, quantity: 2)
19
+ expect { |b| cart.items.each_with_object(&b) }.to yield_successive_args([cart.items.first, bottle])
20
+ end
21
+
22
+ it "should work with items('Type')" do
23
+ cart.add_item(id: 17, type: 'Bottle', name: 'Bordeux', unit_cost: 92.12, cost: 184.24, quantity: 2)
24
+ cart.add_item(id: 27, type: 'UserGiftCard', unit_cost: 25, quantity: 1)
25
+ expect { cart.items('Bottle').each_with_object {} }.not_to raise_error
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,72 @@
1
+ require 'spec_helper'
2
+
3
+ describe AwesomeO do
4
+ describe AwesomeO::Item do
5
+ let(:cart) { AwesomeO::Cart.new(1) }
6
+ let(:item) { cart.add_item(id: 17, type: 'Bottle', name: 'Bordeux', unit_cost: 92.12, quantity: 2) }
7
+
8
+ describe 'data getters and setters' do
9
+ it 'should let me get data stored for the item' do
10
+ expect(item.id).to eq('17')
11
+ expect(item.type).to eq('Bottle')
12
+ expect(item.cost).to eq(184.24)
13
+ end
14
+
15
+ it 'should let me modify data stored for the item' do
16
+ expect(item.quantity).to eq('2')
17
+ item.quantity = 3
18
+ expect(item.quantity).to eq('3')
19
+ end
20
+
21
+ it 'should immediately save data back to redis' do
22
+ expect(item.quantity).to eq('2')
23
+ item.quantity = 3
24
+ new_item = cart.send(:get_item, item._id)
25
+ expect(new_item.quantity).to eq('3')
26
+ end
27
+
28
+ it 'should touch the item and cart' do
29
+ expect { item.quantity = 4 }.to change { item._version }.by(1)
30
+ end
31
+
32
+ it "should raise NoMethodError if you use a getter that there isn't a key for" do
33
+ expect { item.weight }.to raise_error(NoMethodError)
34
+ end
35
+
36
+ it 'should respond_to the getters and setters' do
37
+ expect(item.respond_to?(:name)).to be true
38
+ expect(item.respond_to?(:name=)).to be true
39
+ end
40
+ end
41
+
42
+ describe '#cost' do
43
+ it 'should be equal to the unit_cost multiplied by the quantity' do
44
+ expect(item.cost).to eq(184.24)
45
+ item.quantity = 3
46
+ expect(item.cost).to eq(276.36)
47
+ end
48
+ end
49
+
50
+ describe '#destroy' do
51
+ it 'should remove the item from the cart' do
52
+ item_id = item._id
53
+ item.destroy
54
+ expect(AwesomeO.config.redis.sismember(cart.send(:key), item_id)).to be false
55
+ expect(AwesomeO.config.redis.exists("awesome_o:line_item:#{item_id}")).to be false
56
+ end
57
+ end
58
+
59
+ describe '#touch' do
60
+ it 'should record that the record was changed' do
61
+ item.touch
62
+ expect(item._version).to eq(1)
63
+ end
64
+ end
65
+
66
+ describe '#cache_key' do
67
+ it 'should return item/{id}-{version}' do
68
+ expect(item.cache_key).to eq("item/#{item._id}-#{item._version}")
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,9 @@
1
+ unless Kernel.respond_to?(:require_relative)
2
+ module Kernel
3
+ def require_relative(path)
4
+ require File.join(File.dirname(caller[0]), path.to_str)
5
+ end
6
+ end
7
+ end
8
+
9
+ require_relative '../lib/awesome_o'
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: awesome_o
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Evan Alter
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-10-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: redis
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 3.0.0
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 3.0.0
41
+ description: AwesomeO is a frameworke agnostic, redis-backed, shopping cart system
42
+ email:
43
+ - evan.alter@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".gitignore"
49
+ - ".travis.yml"
50
+ - Gemfile
51
+ - LICENSE.txt
52
+ - README.md
53
+ - Rakefile
54
+ - awesome_o.gemspec
55
+ - lib/awesome_o.rb
56
+ - lib/awesome_o/cart.rb
57
+ - lib/awesome_o/configuration.rb
58
+ - lib/awesome_o/item.rb
59
+ - lib/awesome_o/item_collection.rb
60
+ - lib/awesome_o/version.rb
61
+ - spec/cart_spec.rb
62
+ - spec/item_collection_spec.rb
63
+ - spec/item_spec.rb
64
+ - spec/spec_helper.rb
65
+ homepage: http://github.com/hadees/awesome-o
66
+ licenses: []
67
+ metadata: {}
68
+ post_install_message:
69
+ rdoc_options: []
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ requirements: []
83
+ rubyforge_project:
84
+ rubygems_version: 2.5.1
85
+ signing_key:
86
+ specification_version: 4
87
+ summary: Originaly forked from the gem Cartman
88
+ test_files:
89
+ - spec/cart_spec.rb
90
+ - spec/item_collection_spec.rb
91
+ - spec/item_spec.rb
92
+ - spec/spec_helper.rb