vvm 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ 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: []