rate-card 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/LICENSE.txt +21 -0
- data/README.md +226 -0
- data/exe/rate-card +101 -0
- data/lib/rate_card/client.rb +124 -0
- data/lib/rate_card/constants/addresses.rb +101 -0
- data/lib/rate_card/constants/carriers.rb +49 -0
- data/lib/rate_card/constants/cubic_tiers.rb +67 -0
- data/lib/rate_card/csv_writer.rb +58 -0
- data/lib/rate_card/failure.rb +8 -0
- data/lib/rate_card/grid.rb +184 -0
- data/lib/rate_card/input.rb +32 -0
- data/lib/rate_card/run_spec.rb +164 -0
- data/lib/rate_card/runner.rb +43 -0
- data/lib/rate_card/service.rb +38 -0
- data/lib/rate_card/service_catalog.rb +62 -0
- data/lib/rate_card/shipment.rb +78 -0
- data/lib/rate_card/table_renderer.rb +78 -0
- data/lib/rate_card/token.rb +48 -0
- data/lib/rate_card/token_prompt.rb +58 -0
- data/lib/rate_card/tui/app.rb +521 -0
- data/lib/rate_card/tui/fields/multi_select.rb +120 -0
- data/lib/rate_card/tui/fields/select.rb +80 -0
- data/lib/rate_card/tui/fields/text.rb +73 -0
- data/lib/rate_card/tui/messages.rb +59 -0
- data/lib/rate_card/tui/theme.rb +51 -0
- data/lib/rate_card/ui.rb +126 -0
- data/lib/rate_card/version.rb +5 -0
- data/lib/rate_card/warning.rb +31 -0
- data/lib/rate_card.rb +51 -0
- metadata +189 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RateCard
|
|
4
|
+
module TUI
|
|
5
|
+
module Fields
|
|
6
|
+
# Pick exactly one. Bubbles::List would do this, but it brings a title
|
|
7
|
+
# bar, pagination and a filter this wizard has no use for, and it cannot
|
|
8
|
+
# be shared with MultiSelect below — so both are built on one small
|
|
9
|
+
# cursor-and-window core instead.
|
|
10
|
+
class Select
|
|
11
|
+
WINDOW = 8
|
|
12
|
+
|
|
13
|
+
attr_reader :label
|
|
14
|
+
|
|
15
|
+
# choices: Array<[display String, value]>
|
|
16
|
+
# selected: index the cursor starts on, so a field re-entered by going
|
|
17
|
+
# back opens on the answer it already has rather than on the first row.
|
|
18
|
+
def initialize(label:, choices:, selected: 0)
|
|
19
|
+
@label = label
|
|
20
|
+
@choices = choices
|
|
21
|
+
@cursor = selected
|
|
22
|
+
@value = nil
|
|
23
|
+
@chosen = false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def done? = @chosen
|
|
27
|
+
def value = @value
|
|
28
|
+
|
|
29
|
+
def update(message)
|
|
30
|
+
return nil unless message.is_a?(Bubbletea::KeyMessage)
|
|
31
|
+
|
|
32
|
+
if message.up? || message.to_s == 'k'
|
|
33
|
+
move(-1)
|
|
34
|
+
elsif message.down? || message.to_s == 'j'
|
|
35
|
+
move(1)
|
|
36
|
+
elsif message.enter?
|
|
37
|
+
@value = @choices[@cursor].last
|
|
38
|
+
@chosen = true
|
|
39
|
+
end
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def view
|
|
44
|
+
lines = ["#{Theme.accent(Theme::CURSOR)} #{Theme.bold(@label)}"]
|
|
45
|
+
window.each do |index|
|
|
46
|
+
display = @choices[index].first
|
|
47
|
+
lines << if index == @cursor
|
|
48
|
+
" #{Theme.accent(Theme::CURSOR)} #{Theme.accent(display)}"
|
|
49
|
+
else
|
|
50
|
+
" #{display}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
lines << " #{Theme.muted(hint)}"
|
|
54
|
+
lines.join("\n")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def move(delta)
|
|
60
|
+
@cursor = (@cursor + delta) % @choices.length
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Keeps the cursor inside a fixed-height window so a long service list
|
|
64
|
+
# does not push the banner and earlier answers off the screen.
|
|
65
|
+
def window
|
|
66
|
+
return (0...@choices.length) if @choices.length <= WINDOW
|
|
67
|
+
|
|
68
|
+
start = [[@cursor - (WINDOW / 2), 0].max, @choices.length - WINDOW].min
|
|
69
|
+
(start...(start + WINDOW))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def hint
|
|
73
|
+
parts = ['↑↓ move', 'enter confirm', 'esc back']
|
|
74
|
+
parts << "#{@cursor + 1}/#{@choices.length}" if @choices.length > WINDOW
|
|
75
|
+
parts.join(' · ')
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bubbles'
|
|
4
|
+
|
|
5
|
+
module RateCard
|
|
6
|
+
module TUI
|
|
7
|
+
module Fields
|
|
8
|
+
# A single-line answer, optionally masked and optionally validated.
|
|
9
|
+
#
|
|
10
|
+
# Validation runs on Enter and keeps the field open on failure, so a
|
|
11
|
+
# mistyped zone list or a mangled paste of a JWT costs a keystroke rather
|
|
12
|
+
# than the run — the same guarantee the tty-prompt wizard made.
|
|
13
|
+
class Text
|
|
14
|
+
attr_reader :label, :error
|
|
15
|
+
|
|
16
|
+
# parse: String -> value, raising ArgumentError with a message to show.
|
|
17
|
+
def initialize(label:, parse:, default: nil, mask: false, hint: nil)
|
|
18
|
+
@label = label
|
|
19
|
+
@parse = parse
|
|
20
|
+
@default = default
|
|
21
|
+
@hint = hint
|
|
22
|
+
@error = nil
|
|
23
|
+
@value = nil
|
|
24
|
+
|
|
25
|
+
@input = Bubbles::TextInput.new
|
|
26
|
+
@input.prompt = ''
|
|
27
|
+
@input.placeholder = default.to_s if default
|
|
28
|
+
if mask
|
|
29
|
+
@input.echo_mode = :password
|
|
30
|
+
@input.echo_character = '•'
|
|
31
|
+
end
|
|
32
|
+
@input.focus
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def done? = !@value.nil?
|
|
36
|
+
def value = @value
|
|
37
|
+
|
|
38
|
+
def update(message)
|
|
39
|
+
if message.is_a?(Bubbletea::KeyMessage) && message.enter?
|
|
40
|
+
submit
|
|
41
|
+
return nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
@input, command = @input.update(message)
|
|
45
|
+
command
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def view
|
|
49
|
+
lines = ["#{Theme.accent(Theme::CURSOR)} #{Theme.bold(@label)} #{@input.view}"]
|
|
50
|
+
lines << " #{Theme.muted("#{@hint} · esc back")}" if @hint
|
|
51
|
+
lines << " #{Theme.danger(Theme::CROSS)} #{Theme.danger(@error)}" if @error
|
|
52
|
+
lines.join("\n")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# An empty answer means "take the default"; without this the placeholder
|
|
58
|
+
# would be shown but never actually used.
|
|
59
|
+
def submit
|
|
60
|
+
raw = @input.value.to_s.strip
|
|
61
|
+
raw = @default.to_s if raw.empty? && @default
|
|
62
|
+
|
|
63
|
+
@value = @parse.call(raw)
|
|
64
|
+
@error = nil
|
|
65
|
+
rescue ArgumentError => e
|
|
66
|
+
@error = e.message
|
|
67
|
+
@value = nil
|
|
68
|
+
@input.reset
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bubbletea'
|
|
4
|
+
|
|
5
|
+
module RateCard
|
|
6
|
+
module TUI
|
|
7
|
+
# Results of work done off the event loop. Bubbletea runs a Proc command in
|
|
8
|
+
# a Thread and feeds whatever it returns back through #update, and the fetch
|
|
9
|
+
# pushes ProgressAdvanced from its worker threads via Runner#send.
|
|
10
|
+
class ServicesLoaded < Bubbletea::Message
|
|
11
|
+
attr_reader :services
|
|
12
|
+
|
|
13
|
+
def initialize(services)
|
|
14
|
+
super()
|
|
15
|
+
@services = services
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class LoadFailed < Bubbletea::Message
|
|
20
|
+
attr_reader :error
|
|
21
|
+
|
|
22
|
+
def initialize(error)
|
|
23
|
+
super()
|
|
24
|
+
@error = error
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Carries the running failure count so the bar can show trouble as it
|
|
29
|
+
# happens: on a doomed run the user should not have to wait out all 128
|
|
30
|
+
# calls to find out.
|
|
31
|
+
class ProgressAdvanced < Bubbletea::Message
|
|
32
|
+
attr_reader :completed, :failed
|
|
33
|
+
|
|
34
|
+
def initialize(completed:, failed:)
|
|
35
|
+
super()
|
|
36
|
+
@completed = completed
|
|
37
|
+
@failed = failed
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class FetchFinished < Bubbletea::Message
|
|
42
|
+
attr_reader :grid
|
|
43
|
+
|
|
44
|
+
def initialize(grid)
|
|
45
|
+
super()
|
|
46
|
+
@grid = grid
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class FetchFailed < Bubbletea::Message
|
|
51
|
+
attr_reader :error
|
|
52
|
+
|
|
53
|
+
def initialize(error)
|
|
54
|
+
super()
|
|
55
|
+
@error = error
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'lipgloss'
|
|
4
|
+
|
|
5
|
+
module RateCard
|
|
6
|
+
module TUI
|
|
7
|
+
# Every colour and glyph the TUI uses. Lipgloss strips colour by itself when
|
|
8
|
+
# stdout is not a terminal, so nothing here needs a tty? guard — unlike the
|
|
9
|
+
# Pastel/TTY pairing this replaced, where only Pastel was gated and the
|
|
10
|
+
# spinner and bar leaked escapes into a pipe.
|
|
11
|
+
module Theme
|
|
12
|
+
ACCENT = Lipgloss::AdaptiveColor.new(dark: '#00D3FF', light: '#0072B5')
|
|
13
|
+
OK = Lipgloss::AdaptiveColor.new(dark: '#2ECC71', light: '#1E8449')
|
|
14
|
+
WARNING = Lipgloss::AdaptiveColor.new(dark: '#F5A623', light: '#B7791F')
|
|
15
|
+
DANGER = Lipgloss::AdaptiveColor.new(dark: '#FF5C5C', light: '#C0392B')
|
|
16
|
+
MUTED = Lipgloss::AdaptiveColor.new(dark: '#8A8A8A', light: '#6B6B6B')
|
|
17
|
+
|
|
18
|
+
CURSOR = '❯'
|
|
19
|
+
CHECKED = '◉'
|
|
20
|
+
UNCHECKED = '○'
|
|
21
|
+
TICK = '✓'
|
|
22
|
+
CROSS = '✗'
|
|
23
|
+
BULLET = '▸'
|
|
24
|
+
ALERT = '⚠'
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
def style = Lipgloss::Style.new
|
|
29
|
+
|
|
30
|
+
def accent(text) = style.foreground(ACCENT).render(text)
|
|
31
|
+
def ok(text) = style.foreground(OK).render(text)
|
|
32
|
+
def warning(text) = style.foreground(WARNING).render(text)
|
|
33
|
+
def danger(text) = style.foreground(DANGER).render(text)
|
|
34
|
+
def muted(text) = style.foreground(MUTED).render(text)
|
|
35
|
+
def bold(text) = style.bold(true).render(text)
|
|
36
|
+
|
|
37
|
+
def title(text)
|
|
38
|
+
style.bold(true).foreground(ACCENT).render(text)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The run targets production and cannot be pointed elsewhere, so the
|
|
42
|
+
# banner says so in the one colour the eye catches first.
|
|
43
|
+
def banner
|
|
44
|
+
style.border(Lipgloss::ROUNDED_BORDER)
|
|
45
|
+
.border_foreground(ACCENT)
|
|
46
|
+
.padding(0, 1)
|
|
47
|
+
.render("#{bold('eHub Rate Card Builder')}\n#{danger('●')} production · api.ehub.com")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/rate_card/ui.rb
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'tui/theme'
|
|
4
|
+
|
|
5
|
+
module RateCard
|
|
6
|
+
# Plain, non-interactive output: everything printed before the TUI starts or
|
|
7
|
+
# after it exits — errors, the rate tables, the failure and warning reports,
|
|
8
|
+
# and where the files went.
|
|
9
|
+
#
|
|
10
|
+
# The interactive half (banner, prompts, spinner, progress) moved to TUI::App
|
|
11
|
+
# when this switched to Bubbletea. What stays here is deliberately dumb line
|
|
12
|
+
# printing, so tables and reports survive being piped to a file and specs can
|
|
13
|
+
# still assert against a StringIO.
|
|
14
|
+
class UI
|
|
15
|
+
MAX_LISTED_FAILURES = 10
|
|
16
|
+
MAX_LISTED_WARNINGS = 10
|
|
17
|
+
|
|
18
|
+
def initialize(io: $stdout)
|
|
19
|
+
@io = io
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The one thing printed before the TUI starts, so the run says what it
|
|
23
|
+
# targets before a production token is pasted into it.
|
|
24
|
+
def banner
|
|
25
|
+
@io.puts
|
|
26
|
+
@io.puts TUI::Theme.banner
|
|
27
|
+
@io.puts
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# No newline: the answer is typed on the same line.
|
|
31
|
+
def prompt(message)
|
|
32
|
+
@io.print "#{TUI::Theme.accent(TUI::Theme::CURSOR)} #{TUI::Theme.bold(message)}"
|
|
33
|
+
@io.flush
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def info(message)
|
|
37
|
+
@io.puts "#{TUI::Theme.accent(TUI::Theme::BULLET)} #{message}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def warn(message)
|
|
41
|
+
@io.puts " #{TUI::Theme.warning(TUI::Theme::ALERT)} #{message}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def error(message)
|
|
45
|
+
@io.puts " #{TUI::Theme.danger(TUI::Theme::CROSS)} #{message}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def success(message)
|
|
49
|
+
@io.puts " #{TUI::Theme.ok(TUI::Theme::TICK)} #{message}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def blank
|
|
53
|
+
@io.puts
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def print_tables(tables)
|
|
57
|
+
tables.each do |title, rendered|
|
|
58
|
+
@io.puts
|
|
59
|
+
@io.puts " #{TUI::Theme.bold(title)}"
|
|
60
|
+
@io.puts rendered
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def failure_report(failures, spec:)
|
|
65
|
+
return if failures.empty?
|
|
66
|
+
|
|
67
|
+
@io.puts
|
|
68
|
+
warn "#{failures.length} #{failures.length == 1 ? 'cell' : 'cells'} failed"
|
|
69
|
+
failures.first(MAX_LISTED_FAILURES).each do |failure|
|
|
70
|
+
@io.puts " #{row_prefix(spec)}#{spec.row_label(failure.weight)} Z#{failure.zone} → #{failure.message}"
|
|
71
|
+
end
|
|
72
|
+
remaining = failures.length - MAX_LISTED_FAILURES
|
|
73
|
+
@io.puts " … and #{remaining} more" if remaining.positive?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# What the API itself reported on calls that succeeded. Separate from
|
|
77
|
+
# #failure_report: those cells have no answer, these have an answer that
|
|
78
|
+
# says why there is no rate.
|
|
79
|
+
def warning_report(warnings)
|
|
80
|
+
return if warnings.empty?
|
|
81
|
+
|
|
82
|
+
@io.puts
|
|
83
|
+
noun = warnings.length == 1 ? 'warning' : 'warnings'
|
|
84
|
+
warn "the API reported #{warnings.length} #{noun}"
|
|
85
|
+
|
|
86
|
+
listed = warnings.first(MAX_LISTED_WARNINGS)
|
|
87
|
+
account, service = listed.partition(&:account_wide?)
|
|
88
|
+
warning_group("for this card's services", service)
|
|
89
|
+
warning_group('for every service on this token, including ones not in this card', account)
|
|
90
|
+
|
|
91
|
+
remaining = warnings.length - MAX_LISTED_WARNINGS
|
|
92
|
+
@io.puts " … and #{remaining} more" if remaining.positive?
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Headed rather than filtered. An account-wide message can still be about
|
|
96
|
+
# this card — 'Unable to verify address' names the destination we sent — so
|
|
97
|
+
# dropping the ones that do not name a selected service would delete the
|
|
98
|
+
# most actionable warning in the run. The heading says what it can be
|
|
99
|
+
# blamed on and leaves the reading to the user.
|
|
100
|
+
def warning_group(heading, warnings)
|
|
101
|
+
return if warnings.empty?
|
|
102
|
+
|
|
103
|
+
@io.puts " #{heading}"
|
|
104
|
+
warnings.each do |warning|
|
|
105
|
+
repeat = warning.count > 1 ? " (×#{warning.count})" : ''
|
|
106
|
+
@io.puts " #{warning.message}#{repeat}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def saved(paths)
|
|
111
|
+
return if paths.empty?
|
|
112
|
+
|
|
113
|
+
@io.puts
|
|
114
|
+
noun = paths.length == 1 ? 'file' : 'files'
|
|
115
|
+
success "Saved #{paths.length} #{noun} to #{paths.first.dirname}"
|
|
116
|
+
paths.each { |path| @io.puts " #{path.basename}" }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# "wt " in weight mode, so failure lines read exactly as before; nothing
|
|
120
|
+
# extra in cubic mode, since row_label is already self-describing
|
|
121
|
+
# ("Tier 3", not just "3").
|
|
122
|
+
def row_prefix(spec)
|
|
123
|
+
spec.rate_mode == :cubic ? '' : 'wt '
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RateCard
|
|
4
|
+
# One distinct thing the API told us went wrong, with how many calls said it.
|
|
5
|
+
#
|
|
6
|
+
# Deliberately not keyed by (weight, zone) the way Failure is: a carrier
|
|
7
|
+
# outage repeats the same message on all 128 calls, and the user needs the
|
|
8
|
+
# message once, not a wall of it.
|
|
9
|
+
#
|
|
10
|
+
# `scope` is what the message can be blamed on. :service means it came from
|
|
11
|
+
# the `errors` field of a service in this card, so it explains that service's
|
|
12
|
+
# blank cells. :account means it came from the response-level `warnings`
|
|
13
|
+
# array, which covers every service enabled on the token — one call rates all
|
|
14
|
+
# of them — so it may be about a service nobody asked for. It is still
|
|
15
|
+
# reported, not filtered: 'Unable to verify address' arrives at response level
|
|
16
|
+
# and is about this card's own destination address.
|
|
17
|
+
class Warning < Struct.new(:message, :count, :scope, keyword_init: true)
|
|
18
|
+
SERVICE = :service
|
|
19
|
+
ACCOUNT = :account
|
|
20
|
+
|
|
21
|
+
# Service-scoped is the safe default: it says 'this is about what you
|
|
22
|
+
# selected', which is the claim a caller that did not set a scope is making.
|
|
23
|
+
def scope
|
|
24
|
+
self[:scope] || SERVICE
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def account_wide?
|
|
28
|
+
scope == ACCOUNT
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/rate_card.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'rate_card/version'
|
|
4
|
+
|
|
5
|
+
module RateCard
|
|
6
|
+
# Base for everything this tool raises on purpose.
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
|
|
9
|
+
# The production API rejected the token. Retrying will not help.
|
|
10
|
+
class Unauthorized < Error; end
|
|
11
|
+
|
|
12
|
+
# A rate call failed in a way we chose not to retry (or ran out of retries).
|
|
13
|
+
class RequestFailed < Error; end
|
|
14
|
+
|
|
15
|
+
# The probe call came back with no services at all — nothing to build a card from.
|
|
16
|
+
class NoServices < Error; end
|
|
17
|
+
|
|
18
|
+
# The destination directory cannot be written to.
|
|
19
|
+
class OutputNotWritable < Error; end
|
|
20
|
+
|
|
21
|
+
# A carrier was asked for that has no hand-curated zone chart. Raised rather
|
|
22
|
+
# than defaulted: a zone number only means something against a real carrier's
|
|
23
|
+
# own chart, so substituting another carrier's addresses would produce a card
|
|
24
|
+
# that looks right and is mislabeled.
|
|
25
|
+
class UnsupportedCarrier < Error; end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
require_relative 'rate_card/token'
|
|
29
|
+
require_relative 'rate_card/token_prompt'
|
|
30
|
+
require_relative 'rate_card/service'
|
|
31
|
+
require_relative 'rate_card/failure'
|
|
32
|
+
require_relative 'rate_card/constants/carriers'
|
|
33
|
+
require_relative 'rate_card/constants/addresses'
|
|
34
|
+
require_relative 'rate_card/constants/cubic_tiers'
|
|
35
|
+
require_relative 'rate_card/client'
|
|
36
|
+
require_relative 'rate_card/service_catalog'
|
|
37
|
+
require_relative 'rate_card/input'
|
|
38
|
+
require_relative 'rate_card/run_spec'
|
|
39
|
+
require_relative 'rate_card/shipment'
|
|
40
|
+
require_relative 'rate_card/warning'
|
|
41
|
+
require_relative 'rate_card/grid'
|
|
42
|
+
require_relative 'rate_card/csv_writer'
|
|
43
|
+
require_relative 'rate_card/table_renderer'
|
|
44
|
+
require_relative 'rate_card/tui/theme'
|
|
45
|
+
require_relative 'rate_card/tui/messages'
|
|
46
|
+
require_relative 'rate_card/tui/fields/text'
|
|
47
|
+
require_relative 'rate_card/tui/fields/select'
|
|
48
|
+
require_relative 'rate_card/tui/fields/multi_select'
|
|
49
|
+
require_relative 'rate_card/tui/app'
|
|
50
|
+
require_relative 'rate_card/ui'
|
|
51
|
+
require_relative 'rate_card/runner'
|
metadata
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rate-card
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- eHub
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-09-02 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: base64
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.2'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.2'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: csv
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.3'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.3'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: faraday
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '2.13'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '2.13'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: parallel
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '1.26'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '1.26'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: bubbles
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0.1'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0.1'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: bubbletea
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0.1'
|
|
90
|
+
type: :runtime
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0.1'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: lipgloss
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '0.2'
|
|
104
|
+
type: :runtime
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '0.2'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: ntcharts
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0.1'
|
|
118
|
+
type: :runtime
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '0.1'
|
|
125
|
+
description: |
|
|
126
|
+
rate-card walks you through a carrier, its services, zones, a weight range and a
|
|
127
|
+
package type, fetches every weight x zone cell from the eHub API, prints the rate
|
|
128
|
+
card to your terminal and saves it as CSV.
|
|
129
|
+
email:
|
|
130
|
+
executables:
|
|
131
|
+
- rate-card
|
|
132
|
+
extensions: []
|
|
133
|
+
extra_rdoc_files: []
|
|
134
|
+
files:
|
|
135
|
+
- LICENSE.txt
|
|
136
|
+
- README.md
|
|
137
|
+
- exe/rate-card
|
|
138
|
+
- lib/rate_card.rb
|
|
139
|
+
- lib/rate_card/client.rb
|
|
140
|
+
- lib/rate_card/constants/addresses.rb
|
|
141
|
+
- lib/rate_card/constants/carriers.rb
|
|
142
|
+
- lib/rate_card/constants/cubic_tiers.rb
|
|
143
|
+
- lib/rate_card/csv_writer.rb
|
|
144
|
+
- lib/rate_card/failure.rb
|
|
145
|
+
- lib/rate_card/grid.rb
|
|
146
|
+
- lib/rate_card/input.rb
|
|
147
|
+
- lib/rate_card/run_spec.rb
|
|
148
|
+
- lib/rate_card/runner.rb
|
|
149
|
+
- lib/rate_card/service.rb
|
|
150
|
+
- lib/rate_card/service_catalog.rb
|
|
151
|
+
- lib/rate_card/shipment.rb
|
|
152
|
+
- lib/rate_card/table_renderer.rb
|
|
153
|
+
- lib/rate_card/token.rb
|
|
154
|
+
- lib/rate_card/token_prompt.rb
|
|
155
|
+
- lib/rate_card/tui/app.rb
|
|
156
|
+
- lib/rate_card/tui/fields/multi_select.rb
|
|
157
|
+
- lib/rate_card/tui/fields/select.rb
|
|
158
|
+
- lib/rate_card/tui/fields/text.rb
|
|
159
|
+
- lib/rate_card/tui/messages.rb
|
|
160
|
+
- lib/rate_card/tui/theme.rb
|
|
161
|
+
- lib/rate_card/ui.rb
|
|
162
|
+
- lib/rate_card/version.rb
|
|
163
|
+
- lib/rate_card/warning.rb
|
|
164
|
+
homepage: https://github.com/jxsoren/rate-card-cli
|
|
165
|
+
licenses:
|
|
166
|
+
- MIT
|
|
167
|
+
metadata:
|
|
168
|
+
source_code_uri: https://github.com/jxsoren/rate-card-cli
|
|
169
|
+
rubygems_mfa_required: 'true'
|
|
170
|
+
post_install_message:
|
|
171
|
+
rdoc_options: []
|
|
172
|
+
require_paths:
|
|
173
|
+
- lib
|
|
174
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
175
|
+
requirements:
|
|
176
|
+
- - ">="
|
|
177
|
+
- !ruby/object:Gem::Version
|
|
178
|
+
version: '3.2'
|
|
179
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
180
|
+
requirements:
|
|
181
|
+
- - ">="
|
|
182
|
+
- !ruby/object:Gem::Version
|
|
183
|
+
version: '0'
|
|
184
|
+
requirements: []
|
|
185
|
+
rubygems_version: 3.5.11
|
|
186
|
+
signing_key:
|
|
187
|
+
specification_version: 4
|
|
188
|
+
summary: Interactive CLI that builds a shipping rate card from the eHub API.
|
|
189
|
+
test_files: []
|