vvm 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 02dbf33936d808c62f51e819a4bc292c578147eb4ff83d237cc7cb0724f22b21
4
+ data.tar.gz: a6c9af0911a1742bc94378c2b4fa7fe8a3a825fa0ed317bb9b18085f28884d22
5
+ SHA512:
6
+ metadata.gz: a6c7398c682df253508c1351d3c2ac248c950065e7816944bf220ced475d6fba1df7ccdd9aa3e835e95ac88c58d488b26942ae62581ac3e67183882b9ba9ae6e
7
+ data.tar.gz: 79ed579632d2baea596e9d970ddda04d12f606b700ec6ffe4ee60958442fe0b7848827cfea251a329684660ae48c1f9f10987f0d46aa603b15f2f68f0f01a795
data/exe/vvm ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift("#{__dir__}/../lib")
5
+
6
+ require 'vvm'
7
+
8
+ Vvm::CLI.new.call
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vvm
4
+ class CLI
5
+ module Command
6
+ class Base
7
+ extend Forwardable
8
+
9
+ attr_reader :env
10
+
11
+ def_delegators :env, :machine, :prompt
12
+
13
+ @subclasses = []
14
+
15
+ class << self
16
+ attr_accessor :command_name
17
+
18
+ def inherited(subclass)
19
+ super
20
+ @subclasses << subclass
21
+ end
22
+
23
+ def by_command_name(name)
24
+ @subclasses.detect { |s| s.command_name == name }
25
+ end
26
+ end
27
+
28
+ def initialize(env)
29
+ @env = env
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vvm
4
+ class CLI
5
+ module Command
6
+ class ConfirmDispsense < Base
7
+ self.command_name = :confirm_dispense
8
+
9
+ def call(formatted_coins:)
10
+ prompt.ok "You've got: #{formatted_coins} coins"
11
+
12
+ prompt.yes?('Would you like to return to main menu?') ? env.call(:show_main_menu) : exit
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vvm
4
+ class CLI
5
+ module Command
6
+ class ConfirmProductPick < Base
7
+ self.command_name = :confirm_product_pick
8
+
9
+ def call(picked_product:)
10
+ prompt.ok "You've got: #{picked_product.name}"
11
+
12
+ prompt.yes?('Would you like to return to main menu?') ? env.call(:show_main_menu) : exit
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vvm
4
+ class CLI
5
+ module Command
6
+ class Dispsense < Base
7
+ self.command_name = :dispense
8
+
9
+ def call
10
+ prompt.say("Balance: $#{machine.balance}")
11
+
12
+ formatted_coins = machine.dispense.filter { _1.qty.positive? }.map { "#{_1.qty} x $#{_1.value}" }.join(', ')
13
+
14
+ env.call(:confirm_dispense, { formatted_coins: formatted_coins })
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vvm
4
+ class CLI
5
+ module Command
6
+ class ShowCoins < Base
7
+ self.command_name = :insert_coin
8
+
9
+ def call
10
+ result = prompt.slider(
11
+ 'Coin',
12
+ [0.25, 0.50, 1.00, 2.00, 5.00, 10.00, 50.00, 100.00],
13
+ help: '(Move arrows left and right to set value)',
14
+ format: ':slider $%.2f'
15
+ )
16
+
17
+ machine.insert(result)
18
+
19
+ env.call(:show_main_menu)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vvm
4
+ class CLI
5
+ module Command
6
+ class ShowProducts < Base
7
+ self.command_name = :pick_product
8
+
9
+ def call
10
+ prompt.say("Balance: $#{machine.balance}")
11
+
12
+ result = prompt.select('Please select', inventory_choices.push({ name: 'Back', value: :back }))
13
+
14
+ if result == :back
15
+ env.call(:show_main_menu)
16
+ else
17
+ env.call(:confirm_product_pick, { picked_product: machine.pick(result) })
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def inventory_choices
24
+ machine.inventory.map do |product|
25
+ {
26
+ name: "#{product.name} - $#{product.price} x #{product.qty}",
27
+ value: product.name,
28
+ **maybe_overpriced(product),
29
+ **maybe_out_of_stock(product)
30
+ }
31
+ end
32
+ end
33
+
34
+ def maybe_overpriced(product)
35
+ machine.balance < product.price ? { disabled: '(insufficient funds)' } : {}
36
+ end
37
+
38
+ def maybe_out_of_stock(product)
39
+ product.qty.zero? ? { disabled: '(out of stock)' } : {}
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vvm
4
+ class CLI
5
+ module Command
6
+ class ShowMainMenu < Base
7
+ self.command_name = :show_main_menu
8
+
9
+ def call
10
+ prompt.say("Balance: $#{machine.balance}")
11
+
12
+ choices = [
13
+ { name: 'Insert a coin', value: :insert_coin, **maybe_out_of_stock },
14
+ { name: 'Pick the product', value: :pick_product },
15
+ { name: 'Dispense', value: :dispense, **maybe_empty_balance, **maybe_no_change }
16
+ ]
17
+
18
+ result = prompt.select('Main menu:', choices)
19
+
20
+ env.call(result)
21
+ end
22
+
23
+ private
24
+
25
+ def maybe_empty_balance
26
+ machine.balance.zero? ? { disabled: '(empty balance)' } : {}
27
+ end
28
+
29
+ def maybe_no_change
30
+ machine.coins.all? { _1.qty.zero? } ? { disabled: '(no coins)' } : {}
31
+ end
32
+
33
+ def maybe_out_of_stock
34
+ machine.inventory.all? { _1.qty.zero? } ? { disabled: '(machine is empty)' } : {}
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vvm
4
+ class CLI
5
+ module Command
6
+ class << self
7
+ def call(env, name, options)
8
+ $stdout.clear_screen
9
+
10
+ class_for(name).new(env).call(**options)
11
+ end
12
+
13
+ private
14
+
15
+ def class_for(name)
16
+ Base.by_command_name(name)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vvm
4
+ class CLI
5
+ class Environment
6
+ attr_reader :machine, :prompt
7
+
8
+ def initialize
9
+ @machine = Vvm::Machine.new
10
+ @prompt = TTY::Prompt.new
11
+ end
12
+
13
+ def call(name, options = {})
14
+ Command.call(self, name, options)
15
+ end
16
+ end
17
+ end
18
+ end
data/lib/vvm/cli.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vvm
4
+ class CLI
5
+ def call
6
+ Environment.new.call(:show_main_menu)
7
+ end
8
+ end
9
+ end
data/lib/vvm/error.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vvm
4
+ class Error < StandardError; end
5
+
6
+ class InsufficientBalance < Error; end
7
+
8
+ class ProductNotFound < Error; end
9
+
10
+ class OutOfStock < Error; end
11
+
12
+ class MachineIsEmpty < Error; end
13
+
14
+ class InvalidInput < Error; end
15
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'YAML'
4
+ require 'forwardable'
5
+
6
+ module Vvm
7
+ class Machine
8
+ extend Forwardable
9
+
10
+ GEM_HOME = File.realpath(File.join(File.dirname(__FILE__), '..', '..'))
11
+
12
+ attr_accessor :balance, :state, :inventory, :coins
13
+
14
+ def_delegators :state, :insert, :pick, :dispense
15
+
16
+ def initialize
17
+ @inventory = build_inventory
18
+ @coins = build_coins
19
+ @balance = 0
20
+ @state = Vvm::State::EmptyBalance.new(self)
21
+ end
22
+
23
+ private
24
+
25
+ def config
26
+ @config ||= YAML.load_file(File.join(GEM_HOME, 'config.yaml'))
27
+ end
28
+
29
+ def build_inventory
30
+ config['inventory'].map { Vvm::Model::Product.new(*_1.values) }
31
+ end
32
+
33
+ def build_coins
34
+ config['coins'].map { Vvm::Model::Coin.new(*_1.values) }
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vvm
4
+ module Model
5
+ Coin = Struct.new(:value, :qty)
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vvm
4
+ module Model
5
+ Product = Struct.new(:name, :price, :qty)
6
+ end
7
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vvm
4
+ module State
5
+ class Base
6
+ def initialize(machine)
7
+ @machine = machine
8
+ end
9
+
10
+ def insert(coin)
11
+ raise NotImplementedError
12
+ end
13
+
14
+ def pick(product_name)
15
+ raise NotImplementedError
16
+ end
17
+
18
+ def dispense # rubocop:disable Metrics/AbcSize
19
+ machine.coins.each_with_object([]) do |coin, acc|
20
+ next if coin.value > machine.balance
21
+
22
+ unbalansed_qty = (machine.balance / coin.value).floor
23
+ qty_to_return = unbalansed_qty > coin.qty ? coin.qty : unbalansed_qty
24
+
25
+ coin_to_return = coin.dup
26
+ coin_to_return.qty = qty_to_return
27
+
28
+ acc.push(coin_to_return)
29
+ coin.qty -= qty_to_return
30
+
31
+ machine.balance = (machine.balance - (coin.value * qty_to_return)).round(2)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :machine
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vvm
4
+ module State
5
+ class EmptyBalance < Base
6
+ def insert(coin)
7
+ machine.balance += coin
8
+ machine.state = Vvm::State::PositiveBalance.new(machine) unless coin <= 0
9
+ machine
10
+ end
11
+
12
+ def pick(_product_name)
13
+ raise InsufficientBalance
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vvm
4
+ module State
5
+ class PositiveBalance < Base
6
+ def insert(coin)
7
+ machine.balance += coin
8
+ machine
9
+ end
10
+
11
+ def pick(product_name)
12
+ product = machine.inventory.detect { _1.name == product_name }
13
+
14
+ validate(product)
15
+
16
+ machine.balance = (machine.balance - product.price).round(2)
17
+ product.qty -= 1
18
+
19
+ maybe_change_state
20
+
21
+ product
22
+ end
23
+
24
+ private
25
+
26
+ def validate(product)
27
+ raise ProductNotFound unless product
28
+ raise OutOfStock if product.qty.zero?
29
+ raise InsufficientBalance if machine.balance < product.price
30
+ end
31
+
32
+ def maybe_change_state
33
+ machine.state = Vvm::State::EmptyBalance.new(machine) if machine.balance.zero?
34
+ machine.state = Vvm::State::SoldOut.new(machine) if machine.inventory.all? { _1.qty.zero? }
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vvm
4
+ module State
5
+ class SoldOut < Base
6
+ def insert(_coin)
7
+ raise MachineIsEmpty
8
+ end
9
+
10
+ def pick(_product_name)
11
+ raise MachineIsEmpty
12
+ end
13
+ end
14
+ end
15
+ end
data/lib/vvm/state.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'vvm/state/base'
4
+ require 'vvm/state/empty_balance'
5
+ require 'vvm/state/positive_balance'
6
+ require 'vvm/state/sold_out'
7
+
8
+ module Vvm
9
+ module State
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vvm
4
+ VERSION = '0.1.0'
5
+ end
data/lib/vvm.rb ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tty-prompt'
4
+
5
+ require_relative 'vvm/version'
6
+ require_relative 'vvm/cli'
7
+ require_relative 'vvm/machine'
8
+ require_relative 'vvm/model/coin'
9
+ require_relative 'vvm/model/product'
10
+ require_relative 'vvm/error'
11
+ require_relative 'vvm/state'
12
+ require_relative 'vvm/cli/environment'
13
+ require_relative 'vvm/cli/command'
14
+ require_relative 'vvm/cli/command/base'
15
+ require_relative 'vvm/cli/command/insert_coin'
16
+ require_relative 'vvm/cli/command/show_main_menu'
17
+ require_relative 'vvm/cli/command/pick_product'
18
+ require_relative 'vvm/cli/command/dispense'
19
+ require_relative 'vvm/cli/command/confirm_dispense'
20
+ require_relative 'vvm/cli/command/confirm_product_pick'
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vvm
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Alex Bykov
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-10-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: tty-prompt
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.23.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.23.1
27
+ description: CLI app which emulates the goods buying operations and change calculation.
28
+ email:
29
+ - leksster@gmail.com
30
+ executables:
31
+ - vvm
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - exe/vvm
36
+ - lib/vvm.rb
37
+ - lib/vvm/cli.rb
38
+ - lib/vvm/cli/command.rb
39
+ - lib/vvm/cli/command/base.rb
40
+ - lib/vvm/cli/command/confirm_dispense.rb
41
+ - lib/vvm/cli/command/confirm_product_pick.rb
42
+ - lib/vvm/cli/command/dispense.rb
43
+ - lib/vvm/cli/command/insert_coin.rb
44
+ - lib/vvm/cli/command/pick_product.rb
45
+ - lib/vvm/cli/command/show_main_menu.rb
46
+ - lib/vvm/cli/environment.rb
47
+ - lib/vvm/error.rb
48
+ - lib/vvm/machine.rb
49
+ - lib/vvm/model/coin.rb
50
+ - lib/vvm/model/product.rb
51
+ - lib/vvm/state.rb
52
+ - lib/vvm/state/base.rb
53
+ - lib/vvm/state/empty_balance.rb
54
+ - lib/vvm/state/positive_balance.rb
55
+ - lib/vvm/state/sold_out.rb
56
+ - lib/vvm/version.rb
57
+ homepage: https://github.com/leksster/vvm
58
+ licenses:
59
+ - MIT
60
+ metadata:
61
+ homepage_uri: https://github.com/leksster/vvm
62
+ source_code_uri: https://github.com/leksster/vvm
63
+ changelog_uri: https://github.com/leksster/vvm
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 3.0.2
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubygems_version: 3.2.22
80
+ signing_key:
81
+ specification_version: 4
82
+ summary: Vvm
83
+ test_files: []