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 +7 -0
- data/exe/vvm +8 -0
- data/lib/vvm/cli/command/base.rb +34 -0
- data/lib/vvm/cli/command/confirm_dispense.rb +17 -0
- data/lib/vvm/cli/command/confirm_product_pick.rb +17 -0
- data/lib/vvm/cli/command/dispense.rb +19 -0
- data/lib/vvm/cli/command/insert_coin.rb +24 -0
- data/lib/vvm/cli/command/pick_product.rb +44 -0
- data/lib/vvm/cli/command/show_main_menu.rb +39 -0
- data/lib/vvm/cli/command.rb +21 -0
- data/lib/vvm/cli/environment.rb +18 -0
- data/lib/vvm/cli.rb +9 -0
- data/lib/vvm/error.rb +15 -0
- data/lib/vvm/machine.rb +37 -0
- data/lib/vvm/model/coin.rb +7 -0
- data/lib/vvm/model/product.rb +7 -0
- data/lib/vvm/state/base.rb +40 -0
- data/lib/vvm/state/empty_balance.rb +17 -0
- data/lib/vvm/state/positive_balance.rb +38 -0
- data/lib/vvm/state/sold_out.rb +15 -0
- data/lib/vvm/state.rb +11 -0
- data/lib/vvm/version.rb +5 -0
- data/lib/vvm.rb +20 -0
- metadata +83 -0
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,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
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
|
data/lib/vvm/machine.rb
ADDED
@@ -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,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
|
data/lib/vvm/state.rb
ADDED
data/lib/vvm/version.rb
ADDED
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: []
|