Weekend Coding Kata Challenge – Coffee Machine Kata in Statically-Typed Ruby

I tried doing the coffee kata challenge the other day, https://simcap.github.io/coffeemachine/. I thought it would be quite fun to revisit my first language that got me my first job as a web developer, Ruby. I haven’t done Ruby and Rspec for so long. It wasn’t so hard to update the Ruby version in my machine and got going.

I decided to create an additional challenge by using statically-typed version of Ruby, using Airbnb’s Sorbet gem https://sorbet.org/.

Overall, the kata was quite challenging for me. Due to the way I structured my code. It was quite difficult to move beyond the third iteration of the kata :D. It’s a bit weird to be writing Ruby again using TDD. I don’t think I can get used to duck-typed language anymore. I have a better appreciation of Kotlin, Java and Typescript lately.

  • Setting up Ruby and Rspec stack is quite verbose and not so straight-forward compared to other stacks. Too many files folders, in my opinion, to get started. I prefer to have only two files and go.
  • Sorbet’s CLI tool is quite interesting – but since it cannot be run together with the TDD stack it can be a bit annoying. Also, there is no IDE plugin for Sorbet to help with writing the code.

I definitely suck at this kata. I need to revisit this soon with other stacks I’m more familiar with recently.

PS: Sorbet’s annotations are really whacky. It’s kinda interesting though.

Here is the repo to the solution:

https://github.com/suryast/coffee-kata-rb

# frozen_string_literal: true

# typed: strict
# define class CoffeeCart

# Implementation of CoffeeCart
class CoffeeCart
  extend T::Sig

  module PriceEnum
    TEA = 0.4
    COFFEE = 0.6
    CHOCOLATE = 0.5
    ORANGE_JUICE = 0.6
  end

  sig { params(drink: String, sugar: Integer, money: Float, extra_hot: T::Boolean).returns(String) }

  def get_order(drink, sugar, money, extra_hot = false)
    stick = sugar.positive? ? 0 : ''
    sugar = sugar.zero? ? '' : sugar

    extra_hot ? drink = (drink + 'h') : drink

    missing_money = send_missing_amount(drink, money)

    # return
    correct_change?(drink, money) ? (drink + ':' + sugar.to_s + ':' + stick.to_s) : send_message(missing_money.to_s)
  end

  sig { params(_message: String).returns(String) }

  def send_message(_message)
    'M:' + _message.to_s
  end

  sig { params(drink: String, money: Float).returns(T::Boolean) }

  def correct_change?(drink, money)
    T.must(send_missing_amount(drink, money)).zero? ? true : false
  end

  sig { params(drink: String, money: Float).returns(T.nilable(T.any(Integer, Float))) }

  def send_missing_amount(drink, money)
    if !(drink == 'T' && money >= PriceEnum::TEA ||
        drink == 'C' && money >= PriceEnum::COFFEE ||
        drink == 'H' && money >= PriceEnum::CHOCOLATE ||
        drink == 'O' && money >= PriceEnum::ORANGE_JUICE)
      missing_amount(drink, money)
    else
      0.0
    end
  end

  sig { params(drink: String, money: Float).returns(T.nilable(T.any(Integer, Float))) }

  def missing_amount(drink, money)
    if drink == 'T' || drink == 'Th'
      (PriceEnum::TEA - money).round(1)
    elsif drink == 'C' || drink == 'Ch'
      (PriceEnum::COFFEE - money).round(1)
    elsif drink == 'H' || drink = 'Hh'
      (PriceEnum::CHOCOLATE - money).round(1)
    else
      (PriceEnum::ORANGE_JUICE - money).round(1)
    end
  end

  private :correct_change?
  private :send_missing_amount
  private :missing_amount
end
# typed: false
# spec/test_spec.rb

require 'sorbet-runtime'
require 'rspec/sorbet'
require 'coffee_cart'

describe CoffeeCart do
  describe '#receive_order' do
    it 'should receive instruction to make drink with sugar' do
      # GIVEN
      drink = 'T'
      sugar = 1
      money = 0.4

      # WHEN
      result = CoffeeCart.new.get_order(drink, sugar, money)

      # THEN
      expect(result).to eq('T:1:0')
    end

    it 'should receive instruction to make drinks with no sugar' do
      # GIVEN
      drink = 'H'
      sugar = 0
      money = 0.5

      # WHEN
      result = CoffeeCart.new.get_order(drink, sugar, money)

      # THEN
      expect(result).to eq('H::')
    end

    it 'should receive instruction to make drinks with two sugar and add a stick' do
      # GIVEN
      drink = 'C'
      sugar = 2
      money = 0.6

      # WHEN
      result = CoffeeCart.new.get_order(drink, sugar, money)

      # THEN
      expect(result).to eq('C:2:0')
    end

    it 'should only make drink if the correct amount of money is given' do
      # GIVEN
      drink = 'T'
      sugar = 1
      money = 0.4

      # WHEN
      result = CoffeeCart.new.get_order(drink, sugar, money)

      # THEN
      expect(result).to eq('T:1:0')
    end

    it 'should return message with how much money missing for the order' do
      # GIVEN
      drink = 'T'
      sugar = 1
      money = 0.3

      # WHEN
      result = CoffeeCart.new.get_order(drink, sugar, money)

      # THEN
      expect(result).to eq('M:0.1')
    end

    it 'should make orange juice for 0.6 euro' do
      # GIVEN
      drink = 'O'
      sugar = 0
      money = 0.6

      # WHEN
      result = CoffeeCart.new.get_order(drink, sugar, money)

      # THEN
      expect(result).to eq('O::')
    end

    it 'should be extra hot drink when required' do
      # GIVEN
      drink = 'C'
      sugar = 0
      money = 0.6
      extra_hot = true

      # WHEN
      result = CoffeeCart.new.get_order(drink, sugar, money, extra_hot)

      # THEN
      expect(result).to eq('Ch::')
    end
  end

  describe '#deliver_message' do
    it 'should receive a message with a drink order' do
      # GIVEN
      message = 'Hello'

      # WHEN
      result = CoffeeCart.new.send_message(message)

      # THEN
      expect(result).to eq('M:' + 'Hello')
    end
  end
end