expressir 2.1.18 → 2.1.20

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,342 @@
1
+ require "terminal-table"
2
+ require "json"
3
+ require "yaml"
4
+ require "ruby-progressbar"
5
+
6
+ module Expressir
7
+ module Commands
8
+ class Coverage < Base
9
+ def run(paths)
10
+ if paths.empty?
11
+ exit_with_error "No paths specified. Please provide paths to EXPRESS files or directories."
12
+ end
13
+
14
+ reports = collect_reports(paths)
15
+
16
+ if reports.empty?
17
+ exit_with_error "No valid EXPRESS files were processed. Nothing to report."
18
+ end
19
+
20
+ # Generate output based on format
21
+ case options[:format].downcase
22
+ when "json"
23
+ display_json_output(reports)
24
+ when "yaml"
25
+ display_yaml_output(reports)
26
+ else # Default to text
27
+ display_text_output(reports)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def collect_reports(paths)
34
+ reports = []
35
+
36
+ paths.each do |path|
37
+ handle_path(path, reports)
38
+ end
39
+
40
+ reports
41
+ end
42
+
43
+ def handle_path(path, reports)
44
+ if File.directory?(path)
45
+ handle_directory(path, reports)
46
+ elsif File.extname(path).downcase == ".exp"
47
+ handle_express_file(path, reports)
48
+ elsif [".yml", ".yaml"].include?(File.extname(path).downcase)
49
+ handle_yaml_manifest(path, reports)
50
+ else
51
+ say "Unsupported file type: #{path}"
52
+ end
53
+ end
54
+
55
+ def handle_directory(path, reports)
56
+ say "Processing directory: #{path}"
57
+ exp_files = Dir.glob(File.join(path, "**", "*.exp"))
58
+ if exp_files.empty?
59
+ say "No EXPRESS files found in directory: #{path}"
60
+ return
61
+ end
62
+
63
+ say "Found #{exp_files.size} EXPRESS files to process"
64
+
65
+ # Initialize progress bar for directory files
66
+ progress = ProgressBar.create(
67
+ title: "Processing files",
68
+ total: exp_files.size,
69
+ format: "%t: [%B] %p%% %a [%c/%C] %e",
70
+ output: $stdout,
71
+ )
72
+
73
+ # Parse all files and create a repository with progress tracking
74
+ begin
75
+ repository = Expressir::Express::Parser.from_files(exp_files) do |filename, _schemas, error|
76
+ if error
77
+ say " Error processing #{File.basename(filename)}: #{error.message}"
78
+ end
79
+ progress.increment
80
+ end
81
+ skip_types = parse_skip_types
82
+ report = Expressir::Coverage::Report.from_repository(repository, skip_types)
83
+ reports << report
84
+ rescue StandardError => e
85
+ say "Error processing directory #{path}: #{e.message}"
86
+ end
87
+ end
88
+
89
+ def handle_express_file(path, reports)
90
+ say "Processing file: #{path}"
91
+ begin
92
+ # For a single file, we don't need a progress bar
93
+ skip_types = parse_skip_types
94
+ report = Expressir::Coverage::Report.from_file(path, skip_types)
95
+ reports << report
96
+ rescue StandardError => e
97
+ say "Error processing file #{path}: #{e.message}"
98
+ end
99
+ end
100
+
101
+ def handle_yaml_manifest(path, reports)
102
+ say "Processing YAML manifest: #{path}"
103
+ begin
104
+ schema_list = YAML.load_file(path)
105
+ manifest_dir = File.dirname(path)
106
+
107
+ if schema_list.is_a?(Hash) && schema_list["schemas"]
108
+ schemas_data = schema_list["schemas"]
109
+
110
+ # Handle the nested structure with schema name keys and path values
111
+ if schemas_data.is_a?(Hash)
112
+ schema_files = schemas_data.values.map do |schema_data|
113
+ if schema_data.is_a?(Hash) && schema_data["path"]
114
+ # Make path relative to the manifest location
115
+ File.expand_path(schema_data["path"], manifest_dir)
116
+ end
117
+ end.compact
118
+
119
+ say "Found #{schema_files.size} schema files to process"
120
+ else
121
+ # If it's a direct array of paths (old format)
122
+ schema_files = schemas_data
123
+ end
124
+ elsif schema_list.is_a?(Array)
125
+ schema_files = schema_list
126
+ else
127
+ say "Invalid YAML format. Expected an array of schema paths or a hash with a 'schemas' key."
128
+ return
129
+ end
130
+
131
+ # Initialize progress bar
132
+ if schema_files && !schema_files.empty?
133
+ say "Processing schemas from manifest file"
134
+
135
+ progress = ProgressBar.create(
136
+ title: "Processing schemas",
137
+ total: schema_files.size,
138
+ format: "%t: [%B] %p%% %a [%c/%C] %e",
139
+ output: $stdout,
140
+ )
141
+
142
+ # Process files with progress tracking
143
+ repository = Expressir::Express::Parser.from_files(schema_files) do |filename, _schemas, error|
144
+ if error
145
+ say " Error processing #{File.basename(filename)}: #{error.message}"
146
+ end
147
+ progress.increment
148
+ end
149
+
150
+ # Create and add the report
151
+ skip_types = parse_skip_types
152
+ report = Expressir::Coverage::Report.from_repository(repository, skip_types)
153
+ reports << report
154
+ end
155
+ rescue StandardError => e
156
+ say "Error processing YAML manifest #{path}: #{e.message}"
157
+ say "Debug: schema_list structure: #{schema_list.class}" if schema_list
158
+ end
159
+ end
160
+
161
+ def display_text_output(reports)
162
+ say "\nEXPRESS Documentation Coverage"
163
+ say "=============================="
164
+
165
+ # If multiple reports, display directory coverage first
166
+ if reports.size > 1
167
+ display_directory_coverage(reports)
168
+ end
169
+
170
+ display_file_coverage(reports)
171
+ display_overall_stats(reports)
172
+ end
173
+
174
+ def display_directory_coverage(reports)
175
+ say "\nDirectory Coverage:"
176
+
177
+ # Collect directory data from all reports
178
+ dirs = {}
179
+ reports.each do |report|
180
+ report.directory_reports.each do |dir_report|
181
+ dir = dir_report["directory"]
182
+ dirs[dir] ||= { "total" => 0, "documented" => 0 }
183
+ dirs[dir]["total"] += dir_report["total"]
184
+ dirs[dir]["documented"] += dir_report["documented"]
185
+ end
186
+ end
187
+
188
+ # Create table
189
+ table = Terminal::Table.new(
190
+ title: "Directory Coverage",
191
+ headings: ["Directory", "Total", "Documented", "Coverage %"],
192
+ style: {
193
+ border_x: "-",
194
+ border_y: "|",
195
+ border_i: "+",
196
+ },
197
+ )
198
+
199
+ # Add rows
200
+ dirs.each do |dir, stats|
201
+ coverage = stats["total"].positive? ? (stats["documented"].to_f / stats["total"] * 100).round(2) : 100.0
202
+ table.add_row [dir, stats["total"], stats["documented"], "#{coverage}%"]
203
+ end
204
+
205
+ say table
206
+ end
207
+
208
+ def display_file_coverage(reports)
209
+ say "\nFile Coverage:"
210
+
211
+ # Create table
212
+ table = Terminal::Table.new(
213
+ title: "File Coverage",
214
+ headings: ["File", "Undocumented Entities", "Coverage %"],
215
+ style: {
216
+ border_x: "-",
217
+ border_y: "|",
218
+ border_i: "+",
219
+ },
220
+ )
221
+
222
+ reports.each do |report|
223
+ report.file_reports.each do |file_report|
224
+ file_path = file_report["file"]
225
+
226
+ # Format undocumented entities as "TYPE name, TYPE name, ..."
227
+ undocumented_formatted = file_report["undocumented"].map do |entity_info|
228
+ "#{entity_info['type']} #{entity_info['name']}"
229
+ end.join(", ")
230
+
231
+ coverage = file_report["coverage"].round(2)
232
+ table.add_row [file_path, undocumented_formatted, "#{coverage}%"]
233
+ end
234
+ end
235
+
236
+ say table
237
+ end
238
+
239
+ def display_overall_stats(reports)
240
+ # Get structured report for overall statistics
241
+ overall = build_structured_report(reports)["overall"]
242
+
243
+ table = Terminal::Table.new(
244
+ title: "Overall Documentation Coverage",
245
+ style: {
246
+ border_x: "-",
247
+ border_y: "|",
248
+ border_i: "+",
249
+ },
250
+ )
251
+
252
+ table.add_row ["Coverage Percentage", "#{overall['coverage_percentage']}%"]
253
+ table.add_row ["Total Entities", overall["total_entities"]]
254
+ table.add_row ["Documented Entities", overall["documented_entities"]]
255
+ table.add_row ["Undocumented Entities", overall["undocumented_entities"]]
256
+
257
+ say table
258
+ end
259
+
260
+ def build_structured_report(reports)
261
+ {
262
+ "overall" => {
263
+ "total_entities" => reports.sum { |r| r.total_entities.size },
264
+ "documented_entities" => reports.sum { |r| r.documented_entities.size },
265
+ "undocumented_entities" => reports.sum { |r| r.undocumented_entities.size },
266
+ "coverage_percentage" => if reports.sum { |r| r.total_entities.size }.positive?
267
+ (reports.sum { |r| r.documented_entities.size }.to_f / reports.sum { |r| r.total_entities.size } * 100).round(2)
268
+ else
269
+ 100.0
270
+ end,
271
+ },
272
+ "files" => reports.flat_map(&:file_reports),
273
+ "directories" => reports.flat_map(&:directory_reports),
274
+ }
275
+ end
276
+
277
+ def display_json_output(reports)
278
+ output_file = options[:output] || "coverage_report.json"
279
+ File.write(output_file, JSON.pretty_generate(build_structured_report(reports)))
280
+ say "JSON coverage report written to: #{output_file}"
281
+ end
282
+
283
+ def display_yaml_output(reports)
284
+ output_file = options[:output] || "coverage_report.yaml"
285
+ File.write(output_file, build_structured_report(reports).to_yaml)
286
+ say "YAML coverage report written to: #{output_file}"
287
+ end
288
+
289
+ # Parse and validate the skip_types option
290
+ # @return [Array<String>] Array of validated entity type names
291
+ def parse_skip_types
292
+ skip_types_option = options["exclude"] || options[:exclude]
293
+ return [] unless skip_types_option
294
+
295
+ # Handle both string (comma-separated) and array inputs
296
+ requested_types = if skip_types_option.is_a?(Array)
297
+ skip_types_option.map(&:to_s).map(&:strip).map(&:upcase)
298
+ else
299
+ skip_types_option.split(",").map(&:strip).map(&:upcase)
300
+ end
301
+
302
+ # Validate each type (supports both TYPE and TYPE:SUBTYPE formats)
303
+ requested_types.each do |type|
304
+ validate_skip_type(type)
305
+ end
306
+
307
+ requested_types
308
+ end
309
+
310
+ # Validate a single skip type (supports TYPE:SUBTYPE syntax)
311
+ # @param type [String] The type to validate
312
+ def validate_skip_type(type)
313
+ if type.include?(":")
314
+ # Handle TYPE:SUBTYPE format
315
+ main_type, subtype = type.split(":", 2)
316
+
317
+ # Validate main type
318
+ unless Expressir::Coverage::ENTITY_TYPE_MAP.key?(main_type)
319
+ exit_with_error "Invalid entity type: #{main_type}. " \
320
+ "Valid types are: #{Expressir::Coverage::ENTITY_TYPE_MAP.keys.join(', ')}"
321
+ end
322
+
323
+ # For TYPE, validate subtype
324
+ if main_type == "TYPE"
325
+ unless Expressir::Coverage::TYPE_SUBTYPES.include?(subtype)
326
+ exit_with_error "Invalid TYPE subtype: #{subtype}. " \
327
+ "Valid TYPE subtypes are: #{Expressir::Coverage::TYPE_SUBTYPES.join(', ')}"
328
+ end
329
+ else
330
+ exit_with_error "Subtype syntax (#{type}) is only supported for TYPE entities"
331
+ end
332
+ else
333
+ # Handle simple type format
334
+ unless Expressir::Coverage::ENTITY_TYPE_MAP.key?(type)
335
+ exit_with_error "Invalid entity type: #{type}. " \
336
+ "Valid types are: #{Expressir::Coverage::ENTITY_TYPE_MAP.keys.join(', ')}"
337
+ end
338
+ end
339
+ end
340
+ end
341
+ end
342
+ end
@@ -0,0 +1,13 @@
1
+ module Expressir
2
+ module Commands
3
+ class Format < Base
4
+ def run(path)
5
+ repository = Expressir::Express::Parser.from_file(path)
6
+ repository.schemas.each do |schema|
7
+ say "\n(* Expressir formatted schema: #{schema.id} *)\n"
8
+ say schema.to_s(no_remarks: true)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,53 @@
1
+ module Expressir
2
+ module Commands
3
+ class Validate < Base
4
+ def run(paths)
5
+ no_version = []
6
+ no_valid = []
7
+
8
+ paths.each do |path|
9
+ x = Pathname.new(path).realpath.relative_path_from(Dir.pwd)
10
+ say "Validating #{x}"
11
+ ret = validate_schema(path)
12
+
13
+ if ret.nil?
14
+ no_valid << "Failed to parse: #{x}"
15
+ next
16
+ end
17
+
18
+ ret.each do |schema_id|
19
+ no_version << "Missing version string: schema `#{schema_id}` | #{x}"
20
+ end
21
+ end
22
+
23
+ print_validation_errors(:failed_to_parse, no_valid)
24
+ print_validation_errors(:missing_version_string, no_version)
25
+
26
+ exit 1 unless [no_valid, no_version].all?(&:empty?)
27
+
28
+ say "Validation passed for all EXPRESS schemas."
29
+ end
30
+
31
+ private
32
+
33
+ def validate_schema(path)
34
+ repository = Expressir::Express::Parser.from_file(path)
35
+ repository.schemas.inject([]) do |acc, schema|
36
+ acc << schema.id unless schema.version&.value
37
+ acc
38
+ end
39
+ rescue StandardError
40
+ nil
41
+ end
42
+
43
+ def print_validation_errors(type, array)
44
+ return if array.empty?
45
+
46
+ say "#{'*' * 20} RESULTS: #{type.to_s.upcase.tr('_', ' ')} #{'*' * 20}"
47
+ array.each do |msg|
48
+ say msg
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,9 @@
1
+ module Expressir
2
+ module Commands
3
+ class Version < Base
4
+ def run
5
+ say Expressir::VERSION
6
+ end
7
+ end
8
+ end
9
+ end