diff --git a/04_solo_project/.gitignore b/04_solo_project/.gitignore new file mode 100644 index 0000000000..2c2dc9b747 --- /dev/null +++ b/04_solo_project/.gitignore @@ -0,0 +1 @@ +twilio.env \ No newline at end of file diff --git a/04_solo_project/.rspec b/04_solo_project/.rspec new file mode 100644 index 0000000000..c99d2e7396 --- /dev/null +++ b/04_solo_project/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/04_solo_project/Gemfile b/04_solo_project/Gemfile new file mode 100644 index 0000000000..294a70ac2d --- /dev/null +++ b/04_solo_project/Gemfile @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +# gem "rails" + +gem 'rubocop', '~> 1.39' + +gem 'rspec', '~> 3.12' + +gem 'twilio-ruby', '~> 5.73' + +gem 'dotenv', '~> 2.8' + +gem "twilio_mock", "~> 0.4.6" diff --git a/04_solo_project/Gemfile.lock b/04_solo_project/Gemfile.lock new file mode 100644 index 0000000000..2724b807cd --- /dev/null +++ b/04_solo_project/Gemfile.lock @@ -0,0 +1,79 @@ +GEM + remote: https://rubygems.org/ + specs: + addressable (2.8.1) + public_suffix (>= 2.0.2, < 6.0) + ast (2.4.2) + crack (0.4.5) + rexml + diff-lcs (1.5.0) + dotenv (2.8.1) + faraday (2.7.0) + faraday-net_http (>= 2.0, < 3.1) + ruby2_keywords (>= 0.0.4) + faraday-net_http (3.0.2) + hashdiff (1.0.1) + json (2.6.2) + jwt (2.5.0) + nokogiri (1.13.9-arm64-darwin) + racc (~> 1.4) + parallel (1.22.1) + parser (3.1.2.1) + ast (~> 2.4.1) + public_suffix (5.0.0) + racc (1.6.0) + rainbow (3.1.1) + regexp_parser (2.6.0) + rexml (3.2.5) + rspec (3.12.0) + rspec-core (~> 3.12.0) + rspec-expectations (~> 3.12.0) + rspec-mocks (~> 3.12.0) + rspec-core (3.12.0) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-support (3.12.0) + rubocop (1.39.0) + json (~> 2.3) + parallel (~> 1.10) + parser (>= 3.1.2.1) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.23.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 1.4.0, < 3.0) + rubocop-ast (1.23.0) + parser (>= 3.1.1.0) + ruby-progressbar (1.11.0) + ruby2_keywords (0.0.5) + twilio-ruby (5.73.2) + faraday (>= 0.9, < 3.0) + jwt (>= 1.5, <= 2.5) + nokogiri (>= 1.6, < 2.0) + twilio_mock (0.4.6) + twilio-ruby (~> 5.3, >= 5.3.1) + webmock (~> 3.0, >= 2) + unicode-display_width (2.3.0) + webmock (3.18.1) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) + +PLATFORMS + arm64-darwin-21 + +DEPENDENCIES + dotenv (~> 2.8) + rspec (~> 3.12) + rubocop (~> 1.39) + twilio-ruby (~> 5.73) + twilio_mock (~> 0.4.6) + +BUNDLED WITH + 2.3.23 diff --git a/04_solo_project/README.md b/04_solo_project/README.md new file mode 100644 index 0000000000..d8d2e733b3 --- /dev/null +++ b/04_solo_project/README.md @@ -0,0 +1,48 @@ +# Solo Project + +Here is a project to test your golden square skills overall: + +> As a customer +> So that I can check if I want to order something +> I would like to see a list of dishes with prices. +> +> As a customer +> So that I can order the meal I want +> I would like to be able to select some number of several available dishes. +> +> As a customer +> So that I can verify that my order is correct +> I would like to see an itemised receipt with a grand total. + +Use the `twilio-ruby` gem to implement this next one. You will need to use +doubles too. + +> As a customer +> So that I am reassured that my order will be delivered on time +> I would like to receive a text such as "Thank you! Your order was placed and +> will be delivered before 18:52" after I have ordered. + +Fair warning: if you push your Twilio API Key to a public Github repository, +anyone will be able to see and use it. What are the security implications of +that? How will you keep that information out of your repository? + +A: I plan on storing the private key in twilio.env and adding twilio.env to the .gitignore file - I should be able to access the key in the application files with something like private_key = ENV['TWILIO_KEY'] +Here is a good [article](https://medium.com/coffee-and-codes/the-simplest-and-powerful-ruby-gem-dotenv-74d64cbc5d5d) on the gem dotenv + +## Getting started + +`git clone https://github.com/jillwones/Golden-Square-Challenges.git` +`bundle install` +`add a twilio.env file to the root of 04_solo_project and inside it write:` +`export TWILIO_ACCOUNT_SID=xxxxxxxxxxxxxxxxxxxxxxxxxxxx` +`export TWILIO_AUTH_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxx` + +If you want the confirmation text to go to your phone number and not mine please change the default number in the send_text method in FinishMyOrder class to your phone number. + +## Usage + +`ruby lib/run_on_terminal.rb` + +## Running tests + +`rspec` diff --git a/04_solo_project/design/README.md b/04_solo_project/design/README.md new file mode 100644 index 0000000000..28b417a76d --- /dev/null +++ b/04_solo_project/design/README.md @@ -0,0 +1,313 @@ +# Solo Project Design Recipe + +## 1. Describe the Problem + +> As a customer +> So that I can check if I want to order something +> I would like to see a list of dishes with prices. +> +> As a customer +> So that I can order the meal I want +> I would like to be able to select some number of several available dishes. +> +> As a customer +> So that I can verify that my order is correct +> I would like to see an itemised receipt with a grand total. + +Use the `twilio-ruby` gem to implement this next one. You will need to use +doubles too. + +> As a customer +> So that I am reassured that my order will be delivered on time +> I would like to receive a text such as "Thank you! Your order was placed and +> will be delivered before 18:52" after I have ordered. + + +## 2. Design the Class System + +```ruby +class Menu + attr_reader :menu # testing terminal io here + def initialize(terminal) + # @menu will be an array of hashes, each hash will have the name of the item, the price and quantity + # @terminal will be there so I can test what the method list_available puts to the terminal + end + + def list_available + # puts a the list of available menu items in a nice format + # takes no args + # will call on private method available_items that will return the @menu + # but only the items with a quantity > 0 + end + + private + + def available_items + # will select the menu items with quantity > 0 and return that array + end + + def format_price(price) + # formats the price so its always like 5.00 or 3.50 rather than 5.0 or 3.5 + end +end + +class Order # testing parent - child class here + attr_reader :order + def initialize + # @order will be an empty array + end + + def add(item, menu) + # takes item and menu, item is a string and menu is an instance of the Menu class + # will raise error if the item is not on the menu, it will call on a private method menu_includes_item(item, menu) + # will raise error if item is out of stock, will call on a private method item_out_of_stock(item, menu) + # if neither of these errors are raised then the item as well as its price is << onto the @order array + # the price of the item is fetched from private method price_of_item(item, menu) + # and then another private method is called, stock_of_item_decreases(item, menu) + # returns nothing + end + + private + + def menu_includes_item(item, menu) + # takes item and menu, string and instance of Menu + # will return true if the menu contains that item + # will return false if the menu doesnt contain that item + end + + def item_out_of_stock(item, menu) + # takes item and menu, string and instance of Menu + # will return true if the item is in stock + # will return false if the item is out of stock + end + + def price_of_item(item, menu) + # takes item and menu, string and instance of Menu + # will return the price of the item as a float + end + + def stock_of_item_decreases(item, menu) + # takes item and menu, string and instance of Menu + # after adding an item to @order, this method will decrease the quantity of the that item in the menu instance + # so if the quantity of chips is 2, when you add 2 to your order the quantity will then be 0 + # and if you tried to add a 3rd you'd get an error raised in the public add method that its out of stock + end +end + +class FinishMyOrder # peer class here + def initialize(order, terminal) + # @order = order + # maybe have @order_array = order.order + # @terminal = terminal + + # raise error 'Nothing ordered' if @order_array.empty? + end + + def print_receipt + # prints itemised receipt with a grand total + # will be looking at the @order_array instance variable + # will puts the result, test with double of Kernel + end + + def send_text(number) + # @text = Text.new(requester=Twilio::REST::Client) + # @text.send_text(number) + end +end + +class Text # testing api requests here + def initialize(requester) + # @requester = requester + end + + def send_text(number) + # is called in the FinishMyOrder instance and talks to twilio api and sends text to say order will + # be with them in 20 minutes time from time order was placed + end +end + +# roughly how to use + +burger_restaurant = Menu.new +burger_restaurant.list_available # shows menu items available +my_order = Order.new +my_order.add('burger', burger_restaurant) +my_order.add('chips', burger_restaurant) +finish_my_order = FinishMyOrder.new(my_order) +finish_my_order.receipt # should puts an itemised receipt with grand total +finish_my_order.send_text(+441234512345) # should ultimately send a text to the user that their order will be ready in 20 minutes time + +``` + +## 3. Create Examples as Integration Tests + +```ruby +# 1 +# can add items to instance of Order +burger_restaurant = Menu.new +my_order = Order.new +my_order.add('burger', burger_restaurant) +my_order.add('chips', burger_restaurant) +expect(my_order.order).to eq(['burger', 4.50],['chips', 2.00]) + +# 2 +# adding an item that is not on the menu raises error +burger_restaurant = Menu.new +my_order = Order.new +expect{ my_order.add('random', burger_restaurant) }.to raise_error('Item not on the menu') + +# 2 +# adding an item that is not in stock raises error +# there is only 1 set of chips in stock so I expect adding chips to be fine and adding the second chips to raise the error +burger_restaurant = Menu.new +my_order = Order.new +expect{ my_order.add('chips', burger_restaurant) }.not_to raise_error +expect{ my_order.add('chips', burger_restaurant) }.to raise_error('Item out of stock') + +# 3 +# adding an item to the order reduces its quantity by 1 +burger_restaurant = Menu.new +my_order = Order.new +original_quantity = burger_restaurant.menu.each do |menu_item| + return menu_item[:quantity] if menu_item[:name] == 'burger' + end +# expect original quantity to be its original quantity +my_order.add('burger', burger_restaurant) +# expect original quantity to be original - 1 + +# 3 +# receipt shows all the prices of the ordered items and the grand total +burger_restaurant = Menu.new +my_order = Order.new +my_order.add('burger', burger_restaurant) +my_order.add('chips', burger_restaurant) +finish_my_order = FinishMyOrder.new(my_order, Kernel) +expected = ['Ordered items:', 'burger - £4.50', 'chips - £2.00', 'Grand Total => £6.50'].join("\n") + "\n" +expect(finish_my_order.receipt).to output(expected).to_stdout + +# 4 +# sends a text after ordered + +# not sure how to test TWILIO yet - may build TWILIO section free running style, and then come back and look into how to test - not best practices but I'd quite like to build it out + +# 5 +# raise error if finalising order but nothing ordered +my_order = Order.new +expect{ FinishMyOrder.new(my_order) }.to raise_error('Nothing ordered') +``` + +## 4. Create Examples as Unit Tests + +```ruby +# MENU +# 1 +# creates an instance of Menu +burger_restaurant = Menu.new +expect(burger_restaurant).to be_an_instance_of(Menu) + +# 2 +# menu.menu returns the array of hashes +burger_restaurant = Menu.new +expected = [ + { name: 'burger', price: 4.50, quantity: 10 }, + { name: 'hot dog', price: 3.00, quantity: 10 }, + { name: 'CocaCola', price: 1.00, quantity: 5 }, + { name: 'chips', price: 2.00, quantity: 1 } + ] +expect(burger_restaurant.menu).to eq(expected) + +# 3 +# menu.list returns just available menu items (quantity > 0) +fake_terminal = double(:terminal) +expect(fake_terminal).to receive(:puts).with("Items in stock:") +allow(fake_terminal).to receive(:puts).with("burger - £4.50") +allow(fake_terminal).to receive(:puts).with("hot dog - £3.00") +allow(fake_terminal).to receive(:puts).with("CocaCola - £1.00") +allow(fake_terminal).to receive(:puts).with("chips - £2.00") + + +burger_restaurant = Menu.new +burger_restaurant.list_available + +# ORDER +# 1 +# creates an instance of order +my_order = Order.new +expect(my_order).to be_an_instance_of(Order) + +# 2 +# Order#add will add an item to your order array +my_order = Order.new +menu = double(:menu) +expect(menu).to receive(:menu_includes_item).and_return(true) +expect(menu).to receive(:method menu_includes_item).and_return(true) +expect(menu).to receive(:price_of_item).and_return(2) + +my_order.add('burger', menu) +my_order.add('chips', menu) +expect(my_order.order).to eq(['burger', 2],['chips', 2]) + +# 3 +# Order#add raises error when adding something that is not on the menu +my_order = Order.new +menu = double(:menu) +expect(menu).to receive(:menu_includes_item).and_return(false) + +expect{ my_order.add('random', menu) }.to raise_error('Item not on the menu') + +# 4 +# Order#add raises error when adding something that is on the menu but out of stock +my_order = Order.new +menu = double(:menu) +expect(menu).to receive(:menu_includes_item).and_return(true) +expect(menu).to receive(:method menu_includes_item).and_return(false) + +expect{ my_order.add('chips', menu) }.to raise_error('Item not in stock') + +# 5 +# Order#add will reduce the quantity of the stock by 1 +# adding an item to the order reduces its quantity by 1 +burger_restaurant = double(:menu, menu: [ + { name: 'burger', price: 4.50, quantity: 10 }, + { name: 'hot dog', price: 3.00, quantity: 10 }, + { name: 'CocaCola', price: 1.00, quantity: 5 }, + { name: 'chips', price: 2.00, quantity: 1 } + ]) +my_order = Order.new + +burger_listing = burger_restaurant.menu[0] + +expect(burger_listing).to eq({:name=>"burger", :price=>4.5, :quantity=>10}) +my_order.add('burger', burger_restaurant) +expect(burger_listing).to eq({:name=>"burger", :price=>4.5, :quantity=>9}) + +# FINISHMYORDER +# 1 +# takes an instance of order when initialising +fake_order = double(:order) +finish_my_order = FinishMyOrder.new(fake_order, Kernel) +expect(fake_my_order).to be_an_instance_of(FinishMyOrder) + +# 2 +# FinishMyOrder#receipt prints the receipt for the order +fake_order = double(:order) +terminal = doubke(:terminal) +finish_my_order = (fake_order, terminal) +expected = ['Ordered items:', 'burger - £4.50', 'chips - £2.00', 'Grand Total => £6.50'].join("\n") + "\n" +allow(terminal).to receive(:puts).and_return(expected) + +# 3 +# FinishMyOrder#send_text send the confirmation text + +# TODO + +``` + +## 5. Implement the Behaviour + +For each example you create as a test, implement the behaviour that allows the +class to behave according to your example. + +Then return to step 3 until you have addressed the problem you were given. You +may also need to revise your design, for example if you realise you made a +mistake earlier. diff --git a/04_solo_project/lib/FinishMyOrder.rb b/04_solo_project/lib/FinishMyOrder.rb new file mode 100644 index 0000000000..4f849ad865 --- /dev/null +++ b/04_solo_project/lib/FinishMyOrder.rb @@ -0,0 +1,31 @@ +require_relative './format_price.rb' + +class FinishMyOrder + include FormatPrice + def initialize(order, terminal = Kernel) + @order = order + @order_array = order.order + @terminal = terminal + + raise 'Order empty' if @order_array.empty? + end + + def receipt + @terminal.puts 'What you ordered:' + @order_array.each do |name, price, num_of_items| + @terminal.puts "#{num_of_items}x #{name} - #{format_price(price)}" + end + @terminal.puts "Grand Total: #{format_price(total)}" + end + + def send_text(number = '+447379766090') + @text = Text.new + @text.send_text(number) + end + + private + + def total + @order_array.sum { |_name, price, _quantity| price } + end +end diff --git a/04_solo_project/lib/Menu.rb b/04_solo_project/lib/Menu.rb new file mode 100644 index 0000000000..eefc3486a0 --- /dev/null +++ b/04_solo_project/lib/Menu.rb @@ -0,0 +1,63 @@ +require_relative './format_price.rb' + +class Menu + attr_reader :menu + include FormatPrice + + def initialize(terminal = Kernel, menu = [ + { name: 'burger', price: 4.50, quantity: 10 }, + { name: 'hot dog', price: 3.00, quantity: 10 }, + { name: 'CocaCola', price: 1.00, quantity: 5 }, + { name: 'chips', price: 2.00, quantity: 1 } + ]) + @menu = menu # funky looking initialize so i could test a fake menu, but also so you could initialize with different menus i suppose + @terminal = terminal + end + + def list_available + @terminal.puts 'Items in stock:' + item_num = 1 + available_items.each do |item| + name = item[:name] + price = item[:price] + @terminal.puts "#{item_num}. #{name} - #{format_price(price)}" + item_num += 1 + end + end + + def includes_item(item) + @menu.each do |menu_item| + return true if menu_item[:name] == item + end + false + end + + def item_out_of_stock(item) + @menu.each do |menu_item| + return true if menu_item[:quantity] < 1 and menu_item[:name] == item + end + false + end + + def price_of_item(item) + @menu.each do |menu_item| + return menu_item[:price] if menu_item[:name] == item + end + end + + def stock_of_item_decreases(item, number) + @menu.each do |menu_item| + if menu_item[:name] == item + raise 'Not enough stock to fulfil order, please choose a lower amount' if number > menu_item[:quantity] + + menu_item[:quantity] -= number + end + end + end + + private + + def available_items + @menu.select { |item| item[:quantity] > 0 } + end +end diff --git a/04_solo_project/lib/Order.rb b/04_solo_project/lib/Order.rb new file mode 100644 index 0000000000..adbd8ddae1 --- /dev/null +++ b/04_solo_project/lib/Order.rb @@ -0,0 +1,40 @@ +class Order + attr_reader :order + + def initialize(menu) + @order = [] + @menu = menu + end + + def add(item, number) + raise 'Item not on the menu' unless menu_includes_item(item) + raise 'Item not in stock' if item_out_of_stock(item) + + @order << [item, price_of_item(item, number), number] + stock_of_item_decreases(item, number) + end + + def complete_order + @completed_order = FinishMyOrder.new(self) + @completed_order.receipt + @completed_order.send_text + end + + private + + def menu_includes_item(item) + @menu.includes_item(item) + end + + def item_out_of_stock(item) + @menu.item_out_of_stock(item) + end + + def price_of_item(item, number) + @menu.price_of_item(item) * number + end + + def stock_of_item_decreases(item, number) + @menu.stock_of_item_decreases(item, number) + end +end diff --git a/04_solo_project/lib/Text.rb b/04_solo_project/lib/Text.rb new file mode 100644 index 0000000000..97893470fb --- /dev/null +++ b/04_solo_project/lib/Text.rb @@ -0,0 +1,21 @@ +require 'twilio-ruby' +require 'dotenv' +Dotenv.load('twilio.env') + +class Text + def initialize(requester = Twilio::REST::Client) + @requester = requester + @client = requester.new(ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN']) + end + + def send_text(number) + delivery_time = (Time.now + 1200).strftime('%I:%M %p') + message = @client.messages.create( + body: "Thank you! Your order was placed and will be delivered before #{delivery_time}", + from: '+13022483728', + to: "+#{number}" + ) + + puts message.sid + end +end diff --git a/04_solo_project/lib/format_price.rb b/04_solo_project/lib/format_price.rb new file mode 100644 index 0000000000..c2907e3972 --- /dev/null +++ b/04_solo_project/lib/format_price.rb @@ -0,0 +1,5 @@ +module FormatPrice + def format_price(price) + "£#{format '%.2f', price}" + end +end diff --git a/04_solo_project/lib/run.rb b/04_solo_project/lib/run.rb new file mode 100644 index 0000000000..731a882212 --- /dev/null +++ b/04_solo_project/lib/run.rb @@ -0,0 +1,67 @@ +require_relative './FinishMyOrder' +require_relative './Menu' +require_relative './Order' +require_relative './Text' + +# ---------------------------------- RUN AS DEFAULT --------------------------------- + +# burger restaurant is the default so when nothing is passed to Menu.new the menu is a burger menu + +burger_restaurant = Menu.new +burger_restaurant.list_available +# items in stock are shown in the terminal: + +# Items in stock: +# 1. burger - £4.50 +# 2. hot dog - £3.00 +# 3. CocaCola - £1.00 +# 4. chips - £2.00 + +my_order = Order.new(burger_restaurant) +my_order.add('burger', 6) +my_order.add('CocaCola', 2) +my_order.add('chips', 1) +my_order.complete_order + +# Calling complete_order on my_order will create a FinishMyOrder instance and pass in my_order in the initialize, then it calls #receipt on that FinishMyOrder instance as well as send_text +# Here is the receipt you would see: +=begin +What you ordered: +6x burger - £27.00 +2x CocaCola - £2.00 +1x chips - £2.00 +Grand Total: £31.00 +=end +# then a text is snet to the default number (mine) with a confirmation, if I ordered at 7:05pm the text will say '"Thank you! Your order was placed and will be delivered before 7:25 PM' + +# -------------------------- RUN WITH DIFFERENT MENU -------------------------------- + +chinese_restaurant = Menu.new(Kernel, [ + { name: 'noodles', price: 7.00, quantity: 10 }, + { name: 'satay chicken', price: 3.50, quantity: 10 }, + { name: 'egg fried rice', price: 6.00, quantity: 5 }, + { name: 'roasted duck', price: 9.00, quantity: 5 } +]) +chinese_restaurant.list_available +# shows items in stock + +# Items in stock: +# 1. noodles - £7.00 +# 2. satay chicken - £3.50 +# 3. egg fried rice - £6.00 +# 4. roasted duck - £9.00 + +my_order = Order.new(chinese_restaurant) +my_order.add('noodles', 6) +my_order.add('roasted duck', 3) +my_order.add('satay chicken', 2) +my_order.complete_order +# first the receipt is shown: + +# What you ordered: +# 6x noodles - £42.00 +# 3x roasted duck - £27.00 +# 2x satay chicken - £7.00 +# Grand Total: £76.00 + +# then the confirmation text is sent diff --git a/04_solo_project/lib/run_on_terminal.rb b/04_solo_project/lib/run_on_terminal.rb new file mode 100644 index 0000000000..b155fcbb50 --- /dev/null +++ b/04_solo_project/lib/run_on_terminal.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require_relative './FinishMyOrder' +require_relative './Menu' +require_relative './Order' +require_relative './Text' + +class UserInterface + def initialize + @chinese_restaurant = [ + { name: 'Noodles', price: 12.00, quantity: 10 }, + { name: 'Egg Fried Rice', price: 10.00, quantity: 10 }, + { name: 'Chicken Satay', price: 6.00, quantity: 5 }, + { name: 'Prawn Toast', price: 2.00, quantity: 2 } + ] + @indian_restaurant = [ + { name: 'Chicken Korma', price: 9.00, quantity: 10 }, + { name: 'Chicken Tikka', price: 8.00, quantity: 10 }, + { name: 'Onion Bhaji', price: 3.00, quantity: 5 }, + { name: 'Naan Bread', price: 2.00, quantity: 2 } + ] + end + + def run + puts 'Please choose a restaurant to order from:' + puts 'Enter 1 to order from a burger place' + puts 'Enter 2 to order chinese' + puts 'Enter 3 to order indian' + choice = gets.chomp.to_i + case choice + when 1 + menu = Menu.new + when 2 + menu = Menu.new(Kernel, @chinese_restaurant) + when 3 + menu = Menu.new(Kernel, @indian_restaurant) + end + my_order = Order.new(menu) + puts 'Here is the menu:' + menu.list_available + loop do + puts 'Enter what you would like to order - please type done once you are finished adding items to your order' + item = gets.chomp + break if item == 'done' + + puts 'Enter the quantity you would like to add to the order' + quantity = gets.chomp.to_i + my_order.add(item, quantity) + end + my_order.complete_order + end +end + +ui = UserInterface.new +ui.run diff --git a/04_solo_project/spec/FinishMyOrder_spec.rb b/04_solo_project/spec/FinishMyOrder_spec.rb new file mode 100644 index 0000000000..a54f2c3e47 --- /dev/null +++ b/04_solo_project/spec/FinishMyOrder_spec.rb @@ -0,0 +1,37 @@ +require 'FinishMyOrder' + +# test peer classes and terminal io here + +describe FinishMyOrder do + context 'when initialising' do + it 'creates an instance of FinishMyOrder' do + fake_order = double(:order, order: [['burger', 4.5], ['chips', 2.0]]) + finish_my_order = FinishMyOrder.new(fake_order) + expect(finish_my_order).to be_an_instance_of(FinishMyOrder) + end + + it 'fails if the order is empty' do + fake_order = double(:order, order: []) + expect{ FinishMyOrder.new(fake_order) }.to raise_error('Order empty') + end + end + + describe '#receipt' do + it 'prints the receipt for the order' do + fake_order = double(:order, order: [['burger', 13.5, 3], ['chips', 2.0, 1]]) + terminal = double(:terminal) + expect(terminal).to receive(:puts).with('What you ordered:') + expect(terminal).to receive(:puts).with('3x burger - £13.50') + expect(terminal).to receive(:puts).with('1x chips - £2.00') + expect(terminal).to receive(:puts).with('Grand Total: £15.50') + finish_my_order = FinishMyOrder.new(fake_order, terminal) + finish_my_order.receipt + end + end + + describe '#send_text' do + # it 'sends a confirmation text to the user that order will arrive in 20 minutes from now' do + # sends text, not quite sure how to test this yet + # end + end +end diff --git a/04_solo_project/spec/Menu_spec.rb b/04_solo_project/spec/Menu_spec.rb new file mode 100644 index 0000000000..438986b24f --- /dev/null +++ b/04_solo_project/spec/Menu_spec.rb @@ -0,0 +1,99 @@ +require 'Menu' + +# test terminal io here + +describe Menu do + context 'when initialised' do + it 'creates an instance of Menu' do + burger_restaurant = Menu.new(Kernel) + expect(burger_restaurant).to be_an_instance_of(Menu) + end + end + + describe '#menu' do + it 'returns the value of the instance variable @menu' do + burger_restaurant = Menu.new(Kernel) + expected = [ + { name: 'burger', price: 4.50, quantity: 10 }, + { name: 'hot dog', price: 3.00, quantity: 10 }, + { name: 'CocaCola', price: 1.00, quantity: 5 }, + { name: 'chips', price: 2.00, quantity: 1 } + ] + expect(burger_restaurant.menu).to eq(expected) + end + + context 'when all quantities > 0' do + it 'putses all of the items in the menu' do + fake_terminal = double(:terminal) + expect(fake_terminal).to receive(:puts).with('Items in stock:') + expect(fake_terminal).to receive(:puts).with('1. burger - £4.50') + expect(fake_terminal).to receive(:puts).with('2. hot dog - £3.00') + expect(fake_terminal).to receive(:puts).with('3. CocaCola - £1.00') + expect(fake_terminal).to receive(:puts).with('4. chips - £2.00') + + burger_restaurant = Menu.new(fake_terminal) + burger_restaurant.list_available + end + end + + context 'when some quantities == 0' do + it 'putses only the available items' do + fake_terminal = double(:terminal) + fake_menu = double(:menu, menu: [{ name: 'burger', price: 4.50, quantity: 5}, { name: 'hot dog', price: 3.00, quantity: 0}, { name: 'chips', price: 2.00, quantity: 0 }]) + + expect(fake_menu).to receive(:select).and_return([{ name: 'burger', price: 4.50, quantity: 5 }]) + expect(fake_terminal).to receive(:puts).with('Items in stock:') + expect(fake_terminal).to receive(:puts).with('1. burger - £4.50') + + burger_restaurant = Menu.new(fake_terminal, fake_menu) + burger_restaurant.list_available + end + end + + describe '#includes_item' do + it 'returns true when menu includes the item' do + burger_restaurant = double(:menu, includes_item: true) + expect( burger_restaurant.includes_item('burger') ).to eq(true) + end + + it 'returns false when menu doesnt include the item' do + burger_restaurant = double(:menu, includes_item: false) + expect( burger_restaurant.includes_item('random') ).to eq(false) + end + end + + describe '#item_out_of_stock' do + it 'returns true if item is out of stock' do + burger_restaurant = double(:menu, item_out_of_stock: true) + expect( burger_restaurant.item_out_of_stock('chips') ).to eq(true) + end + + it 'returns false if item is in stock' do + burger_restaurant = double(:menu, item_out_of_stock: false) + expect( burger_restaurant.item_out_of_stock('chips') ).to eq(false) + end + end + + describe '#price_of_item' do + it 'returns the price of an item' do + burger_restaurant = double(:menu, price_of_item: 4.5) + expect(burger_restaurant.price_of_item('burger') ).to eq(4.5) + end + end + + describe '#stock_of_item_decreases' do + it 'decreases the stock number by 4' do + burger_restaurant = Menu.new + my_order = double(:order) + expect(burger_restaurant.menu[0][:quantity]).to eq(10) + burger_restaurant.stock_of_item_decreases('burger', 4) + expect(burger_restaurant.menu[0][:quantity]).to eq(6) + end + it 'fails if you order more than in stock' do + burger_restaurant = Menu.new + my_order = double(:order) + expect{ burger_restaurant.stock_of_item_decreases('burger', 100) }.to raise_error('Not enough stock to fulfil order, please choose a lower amount') + end + end + end +end diff --git a/04_solo_project/spec/Order_spec.rb b/04_solo_project/spec/Order_spec.rb new file mode 100644 index 0000000000..495bdf4879 --- /dev/null +++ b/04_solo_project/spec/Order_spec.rb @@ -0,0 +1,42 @@ +require 'Order' + +# test parent-child classes here + +describe Order do + context 'when initialised' do + it 'creates an instance of Order' do + menu = double(:menu) + my_order = Order.new(menu) + expect(my_order).to be_an_instance_of(Order) + end + end + + describe '#add' do + it 'adds an item to the order array' do + menu = double(:menu) + my_order = Order.new(menu) + expect(menu).to receive(:includes_item).with('burger').and_return(true) + expect(menu).to receive(:item_out_of_stock).with('burger').and_return(false) + expect(menu).to receive(:price_of_item).with('burger').and_return(4.5) + expect(menu).to receive(:stock_of_item_decreases).with('burger', 1) + + my_order.add('burger', 1) + expect(my_order.order).to eq([['burger', 4.5, 1]]) + end + it 'fails when adding something that is not on the menu' do + menu = double(:menu) + my_order = Order.new(menu) + expect(menu).to receive(:includes_item).with('random').and_return(false) + + expect { my_order.add('random', 1) }.to raise_error('Item not on the menu') + end + it 'fails when adding something that is out of stock' do + menu = double(:menu) + my_order = Order.new(menu) + expect(menu).to receive(:includes_item).with('chips').and_return(true) + expect(menu).to receive(:item_out_of_stock).with('chips').and_return(true) + + expect { my_order.add('chips', 1) }.to raise_error('Item not in stock') + end + end +end diff --git a/04_solo_project/spec/Text_spec.rb b/04_solo_project/spec/Text_spec.rb new file mode 100644 index 0000000000..b31ace3840 --- /dev/null +++ b/04_solo_project/spec/Text_spec.rb @@ -0,0 +1,18 @@ +require 'Text' + +# test api requests here + +describe Text do + context 'when initialising' do + it 'creates an instace of Text' do + text = Text.new + expect(text).to be_an_instance_of(Text) + end + end + + describe '#send_text' do + # it 'takes a phone number as arg as send a confirmation text to that number' do + + # end + end +end diff --git a/04_solo_project/spec/integration_spec.rb b/04_solo_project/spec/integration_spec.rb new file mode 100644 index 0000000000..c9d41b2025 --- /dev/null +++ b/04_solo_project/spec/integration_spec.rb @@ -0,0 +1,123 @@ +require 'FinishMyOrder' +require 'Menu' +require 'Order' +require 'Text' + +describe 'integration' do + describe 'looking at the menu' do + context 'when all items are in stock' do + it 'putses the available items' do + burger_restaurant = Menu.new + expected = ['Items in stock:', '1. burger - £4.50', '2. hot dog - £3.00', '3. CocaCola - £1.00', '4. chips - £2.00'].join("\n") + "\n" + expect{ burger_restaurant.list_available }.to output(expected).to_stdout + end + end + + context 'when some items out of stock' do + it 'putses the available items' do + burger_restaurant = Menu.new + my_order = Order.new(burger_restaurant) + my_order.add('chips', 1) + expected = ['Items in stock:', '1. burger - £4.50', '2. hot dog - £3.00', '3. CocaCola - £1.00'].join("\n") + "\n" + expect{ burger_restaurant.list_available }.to output(expected).to_stdout + end + end + + describe '#includes_item' do + it 'returns true when menu includes the item' do + burger_restaurant = Menu.new + expect( burger_restaurant.includes_item('burger') ).to eq(true) + end + + it 'returns false when menu doesnt include the item' do + burger_restaurant = Menu.new + expect( burger_restaurant.includes_item('random') ).to eq(false) + end + end + + describe '#item_out_of_stock' do + it 'returns true if item is out of stock' do + burger_restaurant = Menu.new + my_order = Order.new(burger_restaurant) + my_order.add('chips', 1) + expect( burger_restaurant.item_out_of_stock('chips') ).to eq(true) + end + + it 'returns false if item is in stock' do + burger_restaurant = Menu.new + my_order = Order.new(burger_restaurant) + expect( burger_restaurant.item_out_of_stock('chips') ).to eq(false) + end + end + + describe '#price_of_item' do + it 'returns the price of an item' do + burger_restaurant = Menu.new + expect(burger_restaurant.price_of_item('burger') ).to eq(4.5) + end + end + + describe '#stock_of_item_decreases' do + it 'decreases the stock number by 4' do + burger_restaurant = Menu.new + my_order = Order.new(burger_restaurant) + expect(burger_restaurant.menu[0][:quantity]).to eq(10) + my_order.add('burger', 4) + expect(burger_restaurant.menu[0][:quantity]).to eq(6) + end + it 'fails if you order more than in stock' do + burger_restaurant = Menu.new + my_order = Order.new(burger_restaurant) + expect{ burger_restaurant.stock_of_item_decreases('burger', 100) }.to raise_error('Not enough stock to fulfil order, please choose a lower amount') + end + end + end + describe 'ordering' do + it 'adds items and their prices to the order' do + burger_restaurant = Menu.new + my_order = Order.new(burger_restaurant) + my_order.add('burger', 2) + my_order.add('chips', 1) + expect(my_order.order).to eq([['burger', 9.0, 2],['chips', 2.0, 1]]) + end + + it 'fails when ordering an item not on the menu' do + burger_restaurant = Menu.new + my_order = Order.new(burger_restaurant) + expect{ my_order.add('random', 1) }.to raise_error('Item not on the menu') + end + + it 'fails when adding an item that is out of stock' do + # as quantity of chips on menu is initialized as 1 + # you should be able to add 1 portion of chips to the + # order with no issues, but then the quantity is now 0 + # so if you try to add another portion an error is raised + burger_restaurant = Menu.new + my_order = Order.new(burger_restaurant) + expect{ my_order.add('chips', 1) }.not_to raise_error + expect{ my_order.add('chips', 1) }.to raise_error('Item not in stock') + end + end + + describe 'receipt' do + it 'shows the prices of the ordered items and the grand total' do + burger_restaurant = Menu.new + my_order = Order.new(burger_restaurant) + my_order.add('burger', 1) + my_order.add('chips', 1) + finish_my_order = FinishMyOrder.new(my_order, Kernel) + expected = ['What you ordered:', '1x burger - £4.50', '1x chips - £2.00', 'Grand Total: £6.50'].join("\n") + "\n" + expect{ finish_my_order.receipt }.to output(expected).to_stdout + end + end + + describe 'finishing order' do + it 'fails if nothing was ordered' do + burger_restaurant = Menu.new + my_order = Order.new(burger_restaurant) + expect{ FinishMyOrder.new(my_order) }.to raise_error('Order empty') + end + + # twilio tests to come + end +end \ No newline at end of file diff --git a/04_solo_project/spec/spec_helper.rb b/04_solo_project/spec/spec_helper.rb new file mode 100644 index 0000000000..fec4e15bd5 --- /dev/null +++ b/04_solo_project/spec/spec_helper.rb @@ -0,0 +1,97 @@ +require 'twilio_mock' +# This file was generated by the `rspec --init` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + + # The settings below are suggested to provide a good initial experience + # with RSpec, but feel free to customize to your heart's content. + # # This allows you to limit a spec run to individual examples or groups + # # you care about by tagging them with `:focus` metadata. When nothing + # # is tagged with `:focus`, all examples get run. RSpec also provides + # # aliases for `it`, `describe`, and `context` that include `:focus` + # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + # config.filter_run_when_matching :focus + # + # # Allows RSpec to persist some state between runs in order to support + # # the `--only-failures` and `--next-failure` CLI options. We recommend + # # you configure your source control system to ignore this file. + # config.example_status_persistence_file_path = "spec/examples.txt" + # + # # Limits the available syntax to the non-monkey patched syntax that is + # # recommended. For more details, see: + # # https://relishapp.com/rspec/rspec-core/docs/configuration/zero-monkey-patching-mode + # config.disable_monkey_patching! + # + # # This setting enables warnings. It's recommended, but in some cases may + # # be too noisy due to issues in dependencies. + # config.warnings = true + # + # # Many RSpec users commonly either run the entire suite or an individual + # # file, and it's useful to allow more verbose output when running an + # # individual spec file. + # if config.files_to_run.one? + # # Use the documentation formatter for detailed output, + # # unless a formatter has already been configured + # # (e.g. via a command-line flag). + # config.default_formatter = "doc" + # end + # + # # Print the 10 slowest examples and example groups at the + # # end of the spec run, to help surface which specs are running + # # particularly slow. + # config.profile_examples = 10 + # + # # Run specs in random order to surface order dependencies. If you find an + # # order dependency and want to debug it, you can fix the order by providing + # # the seed, which is printed after each run. + # # --seed 1234 + # config.order = :random + # + # # Seed global randomization in this process using the `--seed` CLI option. + # # Setting this allows you to use `--seed` to deterministically reproduce + # # test failures related to randomization by passing the same `--seed` value + # # as the one that triggered the failure. + # Kernel.srand config.seed +end diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 9adc651647..0000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,21 +0,0 @@ -Tech Test Submission Requirements/Guidelines -====== - -Before submitting your test, please review the requirements/guidelines below. Note that the requirements are mandatory and if you do not satisfy them we won't review your code (we don't mean to be harsh but this is based on the minimum expectations that our hiring partners require when you submit code for tech tests). - -Requirements ------- - -* Make sure you have written your own README that briefly explains your approach to solving the challenge. -* If your code isn't finished it's not ideal but acceptable as long as you explain in your README where you got to and how you would plan to finish the challenge. -* All code must be written test-first - we're looking for 100% test coverage or as near as possible to that figure. - * The test coverage statistics `SimpleCov` generates after your tests will show you what your coverage is like and where it's lacking. -* Ensure all your tests are passing. -* Check your code conforms to the [Rubocop](https://github.com/bbatsov/rubocop) style guide. Run `rubocop`, read and digest what it says, fix the violations and then run `rubocop` again to check. When you're done, commit and push. - * Advanced mode: run `rubocop` before every commit you make and fix mistakes before you even commit! - -Guidelines -------- - -* Ensure you've understood the specification and built the code according to the challenge guidelines. -* Read through [Code Reviews :pill:](https://github.com/makersacademy/course/blob/main/pills/code_reviews.md) to understand what we're looking for in your code. diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 35de4a7f0e..0000000000 --- a/Gemfile +++ /dev/null @@ -1,13 +0,0 @@ -source 'https://rubygems.org' - -ruby '3.0.2' - -group :test do - gem 'rspec' - gem 'simplecov', require: false, group: :test - gem 'simplecov-console', require: false, group: :test -end - -group :development, :test do - gem 'rubocop', '1.20' -end diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 66064703c7..0000000000 --- a/Gemfile.lock +++ /dev/null @@ -1,66 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - ansi (1.5.0) - ast (2.4.2) - diff-lcs (1.4.4) - docile (1.4.0) - parallel (1.20.1) - parser (3.0.2.0) - ast (~> 2.4.1) - rainbow (3.0.0) - regexp_parser (2.1.1) - rexml (3.2.5) - rspec (3.10.0) - rspec-core (~> 3.10.0) - rspec-expectations (~> 3.10.0) - rspec-mocks (~> 3.10.0) - rspec-core (3.10.1) - rspec-support (~> 3.10.0) - rspec-expectations (3.10.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-mocks (3.10.2) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-support (3.10.2) - rubocop (1.20.0) - parallel (~> 1.10) - parser (>= 3.0.0.0) - rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml - rubocop-ast (>= 1.9.1, < 2.0) - ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.11.0) - parser (>= 3.0.1.1) - ruby-progressbar (1.11.0) - simplecov (0.21.2) - docile (~> 1.1) - simplecov-html (~> 0.11) - simplecov_json_formatter (~> 0.1) - simplecov-console (0.9.1) - ansi - simplecov - terminal-table - simplecov-html (0.12.3) - simplecov_json_formatter (0.1.3) - terminal-table (3.0.1) - unicode-display_width (>= 1.1.1, < 3) - unicode-display_width (2.0.0) - -PLATFORMS - ruby - -DEPENDENCIES - rspec - rubocop (= 1.20) - simplecov - simplecov-console - -RUBY VERSION - ruby 3.0.2p107 - -BUNDLED WITH - 2.2.26 diff --git a/LICENSE b/LICENSE deleted file mode 100644 index e3b114595a..0000000000 --- a/LICENSE +++ /dev/null @@ -1,99 +0,0 @@ -Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License - -By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. - -Section 1 – Definitions. - -Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. -Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. -BY-NC-SA Compatible License means a license listed at creativecommons.org/compatiblelicenses, approved by Creative Commons as essentially the equivalent of this Public License. -Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. -Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. -Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. -License Elements means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution, NonCommercial, and ShareAlike. -Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. -Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. -Licensor means the individual(s) or entity(ies) granting rights under this Public License. -NonCommercial means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange. -Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. -Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. -You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. -Section 2 – Scope. - -License grant. -Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: -reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and -produce, reproduce, and Share Adapted Material for NonCommercial purposes only. -Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. -Term. The term of this Public License is specified in Section 6(a). -Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. -Downstream recipients. -Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. -Additional offer from the Licensor – Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply. -No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. -No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). -Other rights. - -Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. -Patent and trademark rights are not licensed under this Public License. -To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes. -Section 3 – License Conditions. - -Your exercise of the Licensed Rights is expressly made subject to the following conditions. - -Attribution. - -If You Share the Licensed Material (including in modified form), You must: - -retain the following if it is supplied by the Licensor with the Licensed Material: -identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); -a copyright notice; -a notice that refers to this Public License; -a notice that refers to the disclaimer of warranties; -a URI or hyperlink to the Licensed Material to the extent reasonably practicable; -indicate if You modified the Licensed Material and retain an indication of any previous modifications; and -indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. -You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. -If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. -ShareAlike. -In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply. - -The Adapter’s License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-NC-SA Compatible License. -You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material. -You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply. -Section 4 – Sui Generis Database Rights. - -Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: - -for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only; -if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and -You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. -For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. -Section 5 – Disclaimer of Warranties and Limitation of Liability. - -Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. -To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. -The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. -Section 6 – Term and Termination. - -This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. -Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: - -automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or -upon express reinstatement by the Licensor. -For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. -For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. -Sections 1, 5, 6, 7, and 8 survive termination of this Public License. -Section 7 – Other Terms and Conditions. - -The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. -Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. -Section 8 – Interpretation. - -For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. -To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. -No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. -Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. -Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” The text of the Creative Commons public licenses is dedicated to the public domain under the CC0 Public Domain Dedication. Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. - -Creative Commons may be contacted at creativecommons.org. diff --git a/README.md b/README.md deleted file mode 100644 index dbcb154e43..0000000000 --- a/README.md +++ /dev/null @@ -1,83 +0,0 @@ -Takeaway Challenge -================== -``` - _________ - r== | | - _ // | M.A. | )))) - |_)//(''''': | | - // \_____:_____.-------D ))))) - // | === | / \ - .:'//. \ \=| \ / .:'':./ ))))) - :' // ': \ \ ''..'--:'-.. ': - '. '' .' \:.....:--'.-'' .' - ':..:' ':..:' - - ``` - -Instructions -------- - -* Feel free to use google, your notes, books, etc. but work on your own -* If you refer to the solution of another coach or student, please put a link to that in your README -* If you have a partial solution, **still check in a partial solution** -* You must submit a pull request to this repo with your code by 9am Monday morning - -Task ------ - -* Fork this repo -* Run the command 'bundle' in the project directory to ensure you have all the gems -* Write a Takeaway program with the following user stories: - -``` -As a customer -So that I can check if I want to order something -I would like to see a list of dishes with prices - -As a customer -So that I can order the meal I want -I would like to be able to select some number of several available dishes - -As a customer -So that I can verify that my order is correct -I would like to check that the total I have been given matches the sum of the various dishes in my order - -As a customer -So that I am reassured that my order will be delivered on time -I would like to receive a text such as "Thank you! Your order was placed and will be delivered before 18:52" after I have ordered -``` - -* Hints on functionality to implement: - * Ensure you have a list of dishes with prices - * The text should state that the order was placed successfully and that it will be delivered 1 hour from now, e.g. "Thank you! Your order was placed and will be delivered before 18:52". - * The text sending functionality should be implemented using Twilio API. You'll need to register for it. It’s free. - * Use the twilio-ruby gem to access the API - * Use the Gemfile to manage your gems - * Make sure that your Takeaway is thoroughly tested and that you use mocks and/or stubs, as necessary to not to send texts when your tests are run - * However, if your Takeaway is loaded into IRB and the order is placed, the text should actually be sent - * Note that you can only send texts in the same country as you have your account. I.e. if you have a UK account you can only send to UK numbers. - -* Advanced! (have a go if you're feeling adventurous): - * Implement the ability to place orders via text message. - -* A free account on Twilio will only allow you to send texts to "verified" numbers. Use your mobile phone number, don't worry about the customer's mobile phone. - -> :warning: **WARNING:** think twice before you push your **mobile number** or **Twilio API Key** to a public space like GitHub :eyes: -> -> :key: Now is a great time to think about security and how you can keep your private information secret. You might want to explore environment variables. - -* Finally submit a pull request before Monday at 9am with your solution or partial solution. However much or little amount of code you wrote please please please submit a pull request before Monday at 9am - - -In code review we'll be hoping to see: - -* All tests passing -* High [Test coverage](https://github.com/makersacademy/course/blob/main/pills/test_coverage.md) (>95% is good) -* The code is elegant: every class has a clear responsibility, methods are short etc. - -Reviewers will potentially be using this [code review rubric](docs/review.md). Referring to this rubric in advance will make the challenge somewhat easier. You should be the judge of how much challenge you want this at this moment. - -Notes on Test Coverage ------------------- - -You can see your [test coverage](https://github.com/makersacademy/course/blob/main/pills/test_coverage.md) when you run your tests. diff --git a/check.sh b/check.sh deleted file mode 100755 index 1469749226..0000000000 --- a/check.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -# This script is used by Makers to ensure that the challenges stay up to date. -# If you're a student, you don't need to run it. But you can if you like! - -set -Eeuo pipefail -trap cleanup SIGINT SIGTERM ERR - -script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P) - -setup_colors() { - if [[ -t 2 ]] && [[ -z "${NO_COLOR-}" ]] && [[ "${TERM-}" != "dumb" ]]; then - NOFORMAT='\033[0m' RED='\033[0;31m' GREEN='\033[0;32m' ORANGE='\033[0;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' YELLOW='\033[1;33m' - else - NOFORMAT='' RED='' GREEN='' ORANGE='' BLUE='' PURPLE='' CYAN='' YELLOW='' - fi -} - -cleanup() { - trap - SIGINT SIGTERM ERR - die "${RED}Checks failed${NOFORMAT}" -} - -msg() { - echo >&2 -e "${1-}" -} - -die() { - local msg=$1 - local code=${2-1} # default exit status 1 - msg "$msg" - exit "$code" -} - -setup_colors - -msg "${BLUE}Integrity check using Ruby version ${ORANGE}$(ruby -v)${NOFORMAT}" -msg "${BLUE} • Installing bundler${NOFORMAT}" -gem install bundler -msg "${BLUE} • Running bundle install${NOFORMAT}" -bundle install -msg "${BLUE} • Running rspec${NOFORMAT}" -bundle exec rspec -msg "${BLUE} • Running rubocop${NOFORMAT}" -bundle exec rubocop -exit 0 diff --git a/docs/review.md b/docs/review.md deleted file mode 100644 index b43def6b17..0000000000 --- a/docs/review.md +++ /dev/null @@ -1,377 +0,0 @@ -# Introduction -Welcome to the code review for Takeaway Challenge! Again, don't worry - you are not expected to have all the answers. The following is a code-review scaffold for Takeaway Challenge that you can follow if you want to. These are common issues to look out for in this challenge - but you may decide to take your own route. - -If you don't feel comfortable giving technical feedback at this stage, try going through this guide with your reviewee and review the code together. - -# Step 0: Checkout and Run tests - -Please checkout your reviewee's code and run their tests. Read the code and try some manual feature tests in IRB. How easy is it to understand the structure of their code? How readable is their code? Did you need to make any cognitive leaps to 'get it'? - -# Step 1: Structure and supporting files - -## Ensure that all gems being used are in Gemfile - -Please do include all the gems you use in your Gemfile. This is an important courtesy to other developers and yourself in the future, so that all the project dependencies can be pulled in whenever the project is checked out on a new machine, e.g. `twilio-ruby` gem - -## README updated - -Every good code base will have its README updated following the [contribution notes](https://github.com/makersacademy/takeaway-challenge/blob/main/CONTRIBUTING.md), i.e. - -* Make sure you have written your own README that briefly explains your approach to solving the challenge. -* If your code isn't finished it's not ideal but acceptable as long as you explain in your README where you got to and how you would plan to finish the challenge. - -The above is a relatively straightforward thing to do that doesn't involve much programming. Pro-tip: work on this while letting your sub-conscious work on those trickier coding problems :-) - -## Instructions in README - -The README is a great place to show the full story of how your app is used (from a user's perspective), e.g. - -```sh -2.2.3 :001 > t = TakeAway.new - => #0.99, "char sui bun"=>3.99, "pork dumpling"=>2.99, "peking duck"=>7.99, "fu-king fried rice"=>5.99}>, @basket={}, @text_provider=#> -2.2.3 :002 > t.read_menu - => {"spring roll"=>0.99, "char sui bun"=>3.99, "pork dumpling"=>2.99, "peking duck"=>7.99, "fu-king fried rice"=>5.99} -2.2.3 :003 > t.order 'spring roll' - => "1x spring roll(s) added to your basket." -2.2.3 :004 > t.order 'spring roll' - => "1x spring roll(s) added to your basket." -2.2.3 :005 > t.order 'spring roll', 4 - => "4x spring roll(s) added to your basket." -2.2.3 :006 > t.basket_summary - => "spring roll x4 = £3.96" -2.2.3 :007 > t.add 'pork dumpling', 3 - => "3x pork dumpling(s) added to your basket." -2.2.3 :008 > t.basket_summary - => "spring roll x4 = £3.96, pork dumpling x3 = £8.97" -2.2.3 :009 > t.total - => "Total: £12.93" -2.2.3 :010 > c.checkout(12.93) -``` - -# Step 2: Tests and \*\_spec.rb files - -## Tests should test real behaviours not stubs - -You may have read about ["Vacuous" tests](https://github.com/makersacademy/airport_challenge/blob/main/docs/review.md#avoid-vacuous-tests) in the airport challenge code review. The example there focused on how we shouldn't test the behaviour of a double; but we can get into similar trouble if we are stubbing a real object, e.g. - -```ruby -it 'sends a payment confirmation text message' do - expect(subject).to receive(:send_sms) - subject.send_sms(20.93) -end -``` - -In the above the `expect(subject).to receive(:send_sms)` command "stubs" out any existing method called `send_sms` on the subject. Using `expect` instead of `allow` means that at the end of the it block, RSpec checks that subject did receive the message `send_sms`, which we have ensured by calling `subject.send_sms`, so this test passes without ever touching the application code. - -You can confirm this test is 'vacuous' by checking that the [test coverage](https://github.com/makersacademy/course/blob/main/pills/test_coverage.md) doesn't change when you remove it. - -In general you shouldn't be stubbing out behaviour on the object under test. The two key exceptions are when you have randomness or a 3rd party API. We saw how to [stub random behaviour](https://github.com/makersacademy/airport_challenge/blob/main/docs/review.md#handling-randomness-in-tests) in the airport challenge code review, but how do we stub a 3rd party API? See the next section. - -## Stubbing the Twilio API - -The Twilio gem provides access to the online Twilio service. If we don't stub out this interaction, we will send test SMS messages every time we run our tests. Not a good thing. - -The simplest approach is to stub out a method that calls the service, for example: - -```ruby -class Takeaway - - def complete_order(price) - send_text("Thank you for your order: £#{total_price}") - end - - def send_text(message) - # this method calls the Twilio API - end - - ... -end -``` - -can be stubbed out like so: - -```ruby -describe Takeaway - subject(:takeaway) { described_class.new } - - before do - allow(takeaway).to receive(:send_text) - end - - it 'sends a payment confirmation text message' do - expect(takeaway).to receive(:send_text).with("Thank you for your order: £20.93") - takeaway.complete_order(20.93) - end -end -``` - -This ensures that Takeaway#complete_order gets some test coverage and that no SMS will be sent by our tests. This is acceptable, but we still don't have very good test coverage. See the pill on [levels of stubbing 3rd party services](https://github.com/makersacademy/course/blob/main/pills/levels_of_stubbing.md) for some alternatives. - -## Unit vs Integration tests - -Note that if you create real objects (not doubles) in your unit tests other than that which is the subject, then you are using the [Chicago style](http://programmers.stackexchange.com/questions/123627/what-are-the-london-and-chicago-schools-of-tdd) of unit testing (also called [integration testing](http://stackoverflow.com/a/7876055/316729)). In general you want to separate up your unit from your integration (or "feature") tests. Unit tests can just rest in the root of the spec folder, but features of integration tests should go in a subfolder (spec/features or spec/integration) or even in a separate folder on the root directory to allow them to run completely separately. - -At Makers Academy we recommend using the London style with doubles to effectively isolate the single class being tested in a unit test. - -```ruby -# London Style -describe Order do - let(:menu) { double :menu, price: '£1.00', contains?: true } - subject(:order) { described_class.new(menu) } - - it 'order total to be sum of items added' do - order.add('Pizza') - order.add('Pizza') - expect(order.total).to eq '£2.00' - end -end -``` - -```ruby -# Chicago Style --> arguably an 'integration' or 'feature' test -describe Order do - subject(:order) { described_class.new(Menu.new) } # note real Menu class - - it 'order total to be sum of items added' do - order.add('Pizza') - order.add('Pizza') - expect(order.total).to eq '£2.00' - end -end -``` - -## Explicit tests for every element of the public interface - -The public interface of a class is any method that can be accessed publicly. So for example: - -```ruby -class Takeaway - - def complete_order(price) - is_correct_amount?(price) - ... - end - - def is_correct_amount?(price) - total_price == price - end -end -``` - -has two public methods, `complete_order` and `is_correct_amount?`, that both should be tested independently `is_correct_amount?` will be implicitly tested by any test of `complete_order`, but since it is public it should have it's own expilcit test. - -However, perhaps `is_correct_amount?` will only ever used internally by Takeaway, and never called by any collaborator objects? In which case we can make the public interface of Takeaway simpler like so: - -```ruby -class Takeaway - - def complete_order(price) - is_correct_amount?(price) - ... - end - - private - - def is_correct_amount?(price) - total_price == price - end -end -``` - -`private` methods do not require tests; although having too many private methods is a design smell. Anyhow, having moved `is_correct_amount?` into a private method we have reduced the complexity of the public interface of Takeaway (a good thing) and we no longer have to test `is_correct_amount?` explicitly, which also slims down our tests. Reducing the complexity of the public interface of any class (both in terms of number of methods, and numbers of arguments they take) is generally a good thing as it reduces the numbers of ways in which the object can interact with other objects, and encourages a loose coupling between objects that promotes re-use. - -# Step 3: Application code and \*.rb files - -## Use of modules - -There are two main uses of modules in Ruby; one is to provide 'utility' libraries (which are sometimes a code smell) and the other is to provide mixins. However, using a module as a mixin can violate the Single Responsibility Principle. Although code is _defined_ in the module, when it is `include`d in a class, its behaviour becomes part of that class and therefore part of the class's responsibilities. Shared behaviour can be refactored into mixins (e.g. `BikeContainer` in Boris Bikes), but other responsibilities the class is dependent on (e.g. sending text messages for the restaurant) should be injected (see [this practical on dependency injection](https://github.com/makersacademy/skills-workshops/blob/f5b4801840fe07d26ff70341652dc81dcda12289/practicals/object_oriented_design/dependency_injection.md)). - -## Law of Demeter - -The [Law of Demeter](https://en.wikipedia.org/wiki/Law_of_Demeter) suggests that: - -* Each object should have only limited knowledge about other units: only units "closely" related to the current unit. -* Each object should only talk to its friends; don't talk to strangers. -* Only talk to your immediate friends. - -The following test shows a process of reaching through a series of related objects. The warning sign is the multiple periods in `subject.menu.dishes.length`. Here we are seeing `Restaurant` is being tested for properties that belong to the menu - effectively they are none of restaurant's business and shouldn't be tested here; and we shouldn't see deep-reaching chains like this in application code either: - -```ruby -describe Restaurant do - it "has 2 items on the menu" do - expect(subject.menu.dishes.length).to eq(2) - end -end -``` - -Note this is different to *method chaining*, which enables the calling of multiple methods _on the same object_ in one line of code. The Demeter violation applies to reaching down through multiple objects. - -## Appropriate use of Dependency Injection - -It is likely that the `Restaurant` (or equivalent) class is dependent on another object to handle the Twilio messaging. If not, then this is a violation of Single Responsibility Principle. In order to invert dependencies and make testing easier, the Twilio class should be _injected_ into the `Restaurant` class. So instead of: - -```ruby -class Restaurant - def initialize() - @mmessager = Messager.new - end -end -... -restaurant = Restaurant.new -``` - -You can have (note you can define a default for the dependency as shown here, but that's optional): -```ruby -class Restaurant - def initialize(messager = Messager.new) - @messager = messager - end -end -... -restaurant = Restaurant.new -# or -restaurant = Restaurant.new(dummy_messager) -# where dummy_messager might be a test double for example -``` - -## Separation of Concerns - -Applications generally comprise a number of *concerns*. For example, pure business logic is a concern; interacting with the user (UI) is a concern; persisting data to a file or database is a concern; and so on. Generally, as well as having a single responsibility, a class should only be involved in one concern (which kind of follows, right?). - -To this end, a class that contains pure business logic should not also be concerned with the User Interface or presentation logic. If your business logic class uses `puts` statements to communicate with the user, then it has poor separation of concerns. Business logic objects should return other objects and status indicators that can be translated in a separate presentation layer into user-friendly messages and interactions. This means our business logic is not constrained to a particular output representation. - -Separation of concerns leads to some very powerful design patterns such as Model View Controller (MVC), which we will meet in Week 4. - -```ruby -# good -class Menu - def to_s - 'the menu is empty' - end -end -``` - -``` -$ menu = Menu.new -$ puts menu # note that the to_s is called by default when `puts`ing an object in Ruby -=> "the menu is empty" -``` - -```ruby -# bad -class Menu - def display - puts 'the menu is empty' - end -end -``` - -``` -$ menu = Menu.new -$ menu.display -=> "the menu is empty" # note that this string will be outputed in your tests etc. -``` - -## Design for Single Responsibility Principle - -It's easy to overlook responsibilities and end up with a class that does too much. This is a great opportunity to refactor your design to extract those responsibilities. One common indication is that a group of methods share a noun. For example, in `Restaurant` we might have: - -```ruby -def add_to_order(item) -... -end - -def order_total -... -end - -def finalize_order -... -end -``` - -The noun 'order' appears in three method names and this is a clear indication that we need an `Order` class. The beauty of OO is that as soon as we extract this responsibility into another class, our design becomes instantly much more powerful. Enabling the restaurant to handle multiple orders is suddenly much easier. - - -## Personal details and tokens on GitHub - -A well ordered codebase will use ENV variables and the [dotenv gem](https://github.com/bkeepers/dotenv) to ensure that sensitive infomration such as phone numbers and security tokens are not pushed up to public repos on Github. - -## Explore the language for solutions to common problems -### Use `Hash.new` to specify defaults other than `nil` - -This can be particularly useful if you are managing counts of things (e.g. dishes). Instead of: -```ruby -def initialize - @items = {} -end - -def add_dish(dish, quantity = 1) - @items[dish] = 0 unless items[dish] - @items[dish] += quantity -end -``` -You can remove the test and initialization in the first line of `add_dish` by defining `0` as the default: -```ruby -def initialize - @items = Hash.new(0) -end - -def add_dish(dish, quantity = 1) - @items[dish] += quantity -end -``` - -### Use `reduce` to aggregate over a collection (e.g. when calculating the total) - -instead of the following (which assumes `@items` is an array rather than a hash): - -```ruby -def total_price - total = 0 - @items.each do |item| - total += item.price - end - total -end -``` -You can use the `reduce` method (alias `inject`) already provided by Ruby: -```ruby -def total_price - @items.reduce { |sum, item| sum + item } -end -``` - -## Open Closed Principle - -The Open Closed Principle tells us that we want our code to be *open* for extension but *closed* for modification. The idea is that if we need to add some new functionality then we can do that by *extending* our code rather than *modifying* it. - -For example the menu items should not be hard coded into a restaurant class. Arguably they should not be in the business logic at all, e.g. being added at runtime (i.e. in IRB) or loaded from an external hash or maybe even a file. We are concerned that the menu items will likely change, so if they are hard coded in like this: - -```ruby -class Restaurant - def initialize - @menu = {} - @menu[:pizza] = 100 - @menu[:coke] = 70 - end -end -``` - -Then in order to make any changes to the menu we have to open up the Restaurant class itself. Restaurant is not open for extension at all in the above example. It must be opened up for modification in order to make any changes to the menu. Consider this alternative: - -```ruby -class Restaurant - def initialize(menu = Menu.new) - @menu = menu - end -end -``` - -The type of Menu that Restaurant uses can now easily be changed. It is "open for extension". We can create a new type of Menu class that loads from a file, takes dynamic user, whatever we want, and Restaurant will happily collaborate (assuming the menu has the correct public interface, e.g. Menu#contains?, Menu#price etc.). This latter Restaurant is also "closed for modification". We don't need to modify because it can easily be extended through the use of different collaborator objects. - -## Use consistent styles and indentation - -The Ruby community has a very consistent style guide and you should follow it. Use tools like [Rubocop](https://github.com/bbatsov/rubocop) (`gem install rubocop ; rubocop`) to analyze your code for violations. - -You may find it difficult to remember the indentation rules, but one helpful rule of thumb for indentation is to ensure that you are two space indented inside any `do ... end`, `class ... end` or `def ... end` block. diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb deleted file mode 100644 index 252747d899..0000000000 --- a/spec/spec_helper.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'simplecov' -require 'simplecov-console' - -SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ - SimpleCov::Formatter::Console, - # Want a nice code coverage website? Uncomment this next line! - # SimpleCov::Formatter::HTMLFormatter -]) -SimpleCov.start - -RSpec.configure do |config| - config.after(:suite) do - puts - puts "\e[33mHave you considered running rubocop? It will help you improve your code!\e[0m" - puts "\e[33mTry it now! Just run: rubocop\e[0m" - end -end