railbow 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.
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "formatters/base"
4
+ require_relative "config"
5
+ require_relative "table"
6
+
7
+ module Railbow
8
+ module RoutesFormatter
9
+ VERB_COLORS = {
10
+ "GET" => Formatters::Base::GREEN,
11
+ "POST" => Formatters::Base::YELLOW,
12
+ "PATCH" => Formatters::Base::CYAN,
13
+ "PUT" => Formatters::Base::CYAN,
14
+ "DELETE" => Formatters::Base::RED
15
+ }.freeze
16
+
17
+ RESET = Formatters::Base::RESET
18
+ BOLD = Formatters::Base::BOLD
19
+ DIM = Formatters::Base::DIM
20
+ CYAN = Formatters::Base::CYAN
21
+
22
+ HELP_TEXT_COLOR = <<~HELP
23
+
24
+ #{BOLD}Railbow Routes Options:#{RESET}
25
+
26
+ #{CYAN}RBW_VERB=GET#{RESET} Show only GET routes
27
+ #{CYAN}RBW_VERB=POST,PUT#{RESET} Show only POST and PUT routes
28
+ #{CYAN}RBW_COMPACT=strip-format#{RESET} Strip (.:format) suffixes
29
+ #{CYAN}RBW_COMPACT=oneline#{RESET} Truncate instead of wrapping
30
+ #{CYAN}RBW_COMPACT=dense#{RESET} Remove cell padding
31
+ #{CYAN}RBW_COMPACT=noheader#{RESET} Hide table header row
32
+ #{CYAN}RBW_COMPACT=maxw:40#{RESET} Cap column widths
33
+ #{CYAN}RBW_COMPACT=hide:prefix#{RESET} Hide a column by name
34
+ #{CYAN}RBW_PLAIN=1#{RESET} Disable Railbow formatting (plain Rails output)
35
+ #{CYAN}RBW_HELP=1#{RESET} Show this help message
36
+
37
+ #{DIM}Combine: RBW_COMPACT=strip-format,dense,noheader#{RESET}
38
+ #{DIM}Auto-disabled when piped, in CI, or when called by an LLM agent.#{RESET}
39
+ #{DIM}Example: RBW_VERB=GET rails routes#{RESET}
40
+
41
+ HELP
42
+
43
+ def section_title(title)
44
+ return super unless tty?
45
+ return if Railbow::Params.help?
46
+
47
+ @buffer << "\n#{BOLD}#{CYAN}#{title}:#{RESET}"
48
+ end
49
+
50
+ def header(routes)
51
+ super unless tty?
52
+ end
53
+
54
+ def section(routes)
55
+ return super unless tty?
56
+
57
+ if Railbow::Params.help?
58
+ unless @help_shown
59
+ @buffer << HELP_TEXT_COLOR
60
+ @help_shown = true
61
+ end
62
+ return
63
+ end
64
+
65
+ strip_format = Railbow::Params.compact_strip_format?
66
+ prepared = routes.map { |r| prepare_route(r, strip_format) }
67
+ prepared = filter_by_verb(prepared)
68
+
69
+ groups = group_routes(prepared)
70
+ groups.each { |label, group| render_group(label, group) }
71
+ end
72
+
73
+ private
74
+
75
+ def group_routes(routes)
76
+ grouped = routes.group_by { |r| r[:reqs].include?("#") ? r[:reqs].split("#").first : "(other)" }
77
+ other = grouped.delete("(other)")
78
+ grouped["(other)"] = other if other
79
+ grouped
80
+ end
81
+
82
+ def filter_by_verb(routes)
83
+ verb_filter = Railbow::Params.verb
84
+ return routes if verb_filter.nil? || verb_filter.strip.empty? || verb_filter.strip.upcase == "ALL"
85
+
86
+ allowed = verb_filter.split(",").map { |v| v.strip.upcase }
87
+ routes.select { |r| (r[:verb].split("|") & allowed).any? }
88
+ end
89
+
90
+ def render_group(label, routes)
91
+ if label
92
+ @buffer << ""
93
+ @buffer << "#{BOLD}#{CYAN}\u2500\u2500 #{label} \u2500\u2500#{RESET}"
94
+ end
95
+
96
+ columns = [
97
+ Table::Column.new(label: "Verb", sticky: true),
98
+ Table::Column.new(label: "URI Pattern", sticky: true),
99
+ Table::Column.new(label: "Controller#Action"),
100
+ Table::Column.new(label: "Prefix")
101
+ ]
102
+
103
+ rows = routes.map { |r|
104
+ [
105
+ colorize_verb(r[:verb]),
106
+ colorize_path(r[:path]),
107
+ colorize_reqs(r[:reqs]),
108
+ r[:name].empty? ? "" : colorize_name(r[:name])
109
+ ]
110
+ }
111
+
112
+ renderer = Table::Renderer.new(
113
+ columns: columns,
114
+ theme: Table::Themes::PLAIN,
115
+ compact: Railbow::Params.compact_options,
116
+ aliases: Railbow::Config.table_aliases
117
+ )
118
+ @buffer << renderer.render(rows)
119
+ end
120
+
121
+ def prepare_route(route, compact)
122
+ r = route.dup
123
+ r[:path] = r[:path].sub("(.:format)", "") if compact
124
+ r
125
+ end
126
+
127
+ def colorize_verb(verb)
128
+ return verb if verb.empty?
129
+
130
+ verb.split("|").map { |v|
131
+ color = VERB_COLORS[v]
132
+ color ? "#{color}#{v}#{RESET}" : v
133
+ }.join("#{DIM}|#{RESET}")
134
+ end
135
+
136
+ def colorize_path(path)
137
+ return path if path.empty?
138
+
139
+ path.gsub(/:[a-z_]+|\*[a-z_]+/) { |match| "#{CYAN}#{match}#{RESET}" }
140
+ end
141
+
142
+ def colorize_reqs(reqs)
143
+ return reqs if reqs.empty?
144
+
145
+ if reqs.include?("#")
146
+ controller, action = reqs.split("#", 2)
147
+ "#{BOLD}#{controller}#{RESET}#{DIM}##{RESET}#{action}"
148
+ else
149
+ "#{DIM}#{reqs}#{RESET}"
150
+ end
151
+ end
152
+
153
+ def colorize_name(name)
154
+ return name if name.empty?
155
+
156
+ "#{DIM}#{name}#{RESET}"
157
+ end
158
+
159
+ def tty?
160
+ !Railbow.plain?
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "formatters/base"
4
+ require_relative "config"
5
+ require_relative "table"
6
+
7
+ module Railbow
8
+ module StatsFormatter
9
+ def to_s
10
+ return super if Railbow.plain?
11
+
12
+ formatter = Formatters::Base.new
13
+
14
+ columns = [
15
+ Table::Column.new(label: "Name", sticky: true),
16
+ Table::Column.new(label: "Lines", align: :right),
17
+ Table::Column.new(label: "LOC", align: :right),
18
+ Table::Column.new(label: "Classes", align: :right),
19
+ Table::Column.new(label: "Methods", align: :right),
20
+ Table::Column.new(label: "M/C", align: :right),
21
+ Table::Column.new(label: "LOC/M", align: :right)
22
+ ]
23
+
24
+ rows = []
25
+ total_lines = 0
26
+ total_loc = 0
27
+ total_classes = 0
28
+ total_methods = 0
29
+ code_loc = 0
30
+ test_loc = 0
31
+
32
+ @pairs.each do |pair|
33
+ name, stats = pair
34
+ lines = stats.lines
35
+ loc = stats.code_lines
36
+ classes = stats.classes
37
+ methods = stats.methods
38
+ mc = (classes > 0) ? (methods.to_f / classes).round(0).to_i : 0
39
+ locm = (methods > 0) ? ((loc.to_f / methods) - 2).round(0).to_i : 0
40
+
41
+ total_lines += lines
42
+ total_loc += loc
43
+ total_classes += classes
44
+ total_methods += methods
45
+
46
+ if stats.test_file?
47
+ test_loc += loc
48
+ else
49
+ code_loc += loc
50
+ end
51
+
52
+ rows << [
53
+ name,
54
+ lines.to_s,
55
+ loc.to_s,
56
+ classes.to_s,
57
+ methods.to_s,
58
+ mc.to_s,
59
+ locm.to_s
60
+ ]
61
+ end
62
+
63
+ # Total row
64
+ total_mc = (total_classes > 0) ? (total_methods.to_f / total_classes).round(0).to_i : 0
65
+ total_locm = (total_methods > 0) ? ((total_loc.to_f / total_methods) - 2).round(0).to_i : 0
66
+
67
+ rows << [
68
+ formatter.bold("Total"),
69
+ formatter.bold(total_lines.to_s),
70
+ formatter.bold(total_loc.to_s),
71
+ formatter.bold(total_classes.to_s),
72
+ formatter.bold(total_methods.to_s),
73
+ formatter.bold(total_mc.to_s),
74
+ formatter.bold(total_locm.to_s)
75
+ ]
76
+
77
+ renderer = Table::Renderer.new(
78
+ columns: columns,
79
+ theme: Table::Themes::WALLS,
80
+ compact: Railbow::Params.compact_options,
81
+ aliases: Railbow::Config.table_aliases
82
+ )
83
+
84
+ output = +"\n"
85
+ output << renderer.render(rows)
86
+ output << "\n"
87
+
88
+ # Code/test ratio
89
+ if code_loc > 0 && test_loc > 0
90
+ ratio = (test_loc.to_f / code_loc).round(1)
91
+ output << "\n #{formatter.bold("Code LOC:")} #{code_loc} " \
92
+ "#{formatter.bold("Test LOC:")} #{test_loc} " \
93
+ "#{formatter.bold("Code to Test Ratio:")} 1:#{ratio}\n"
94
+ end
95
+
96
+ output
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railbow
4
+ module Table
5
+ class Column
6
+ attr_reader :label, :width, :min_width, :max_width, :align, :truncate, :truncate_fn, :sticky
7
+
8
+ def initialize(label:, width: :auto, min_width: nil, max_width: nil, align: :left, truncate: false, truncate_fn: nil, sticky: false)
9
+ @label = label
10
+ @width = width
11
+ @min_width = min_width
12
+ @max_width = max_width
13
+ @align = align
14
+ @truncate = truncate
15
+ @truncate_fn = truncate_fn
16
+ @sticky = sticky
17
+ end
18
+
19
+ def fixed?
20
+ width.is_a?(Integer)
21
+ end
22
+ end
23
+ end
24
+ end