expressir 2.1.18 → 2.1.19

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,68 @@
1
+ module Expressir
2
+ module Commands
3
+ class Benchmark < Base
4
+ def run(path)
5
+ configure_benchmarking
6
+
7
+ if [".yml", ".yaml"].include?(File.extname(path).downcase)
8
+ benchmark_from_yaml(path)
9
+ else
10
+ benchmark_file(path)
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def configure_benchmarking
17
+ Expressir.configuration.benchmark_enabled = true
18
+ Expressir.configuration.benchmark_ips = options[:ips]
19
+ Expressir.configuration.benchmark_verbose = options[:verbose]
20
+ Expressir.configuration.benchmark_save = options[:save]
21
+ Expressir.configuration.benchmark_format = options[:format] if options[:format]
22
+ end
23
+
24
+ def benchmark_file(path)
25
+ say "Express Schema Loading Benchmark"
26
+ say "--------------------------------"
27
+
28
+ repository = Expressir::Benchmark.measure_file(path) do
29
+ Expressir::Express::Parser.from_file(path)
30
+ end
31
+
32
+ if repository
33
+ say "Loaded #{repository.schemas.size} schemas"
34
+ else
35
+ say "Failed to load schema"
36
+ end
37
+ end
38
+
39
+ def benchmark_from_yaml(yaml_path)
40
+ say "Express Schema Loading Benchmark from YAML"
41
+ say "--------------------------------"
42
+
43
+ # Load schema list from YAML
44
+ schema_list = YAML.load_file(yaml_path)
45
+ if schema_list.is_a?(Hash) && schema_list["schemas"]
46
+ # Handle format: { "schemas": ["path1", "path2", ...] }
47
+ schema_files = schema_list["schemas"]
48
+ elsif schema_list.is_a?(Array)
49
+ # Handle format: ["path1", "path2", ...]
50
+ schema_files = schema_list
51
+ else
52
+ say "Invalid YAML format. Expected an array of schema paths or a hash with a 'schemas' key."
53
+ return
54
+ end
55
+
56
+ say "YAML File: #{yaml_path}"
57
+ say "Number of schemas in list: #{schema_files.size}"
58
+ say "--------------------------------"
59
+
60
+ # Load the schemas
61
+ Expressir::Benchmark.measure_collection(schema_files) do |file|
62
+ repository = Expressir::Express::Parser.from_file(file)
63
+ repository.schemas
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,88 @@
1
+ module Expressir
2
+ module Commands
3
+ class BenchmarkCache < Base
4
+ def run(path)
5
+ configure_benchmarking
6
+
7
+ # Run benchmarks with cache
8
+ cache_path = options[:cache_path] || generate_temp_cache_path
9
+
10
+ if [".yml", ".yaml"].include?(File.extname(path).downcase)
11
+ benchmark_cache_from_yaml(path, cache_path)
12
+ else
13
+ benchmark_cache_file(path, cache_path)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def configure_benchmarking
20
+ Expressir.configuration.benchmark_enabled = true
21
+ Expressir.configuration.benchmark_ips = options[:ips]
22
+ Expressir.configuration.benchmark_verbose = options[:verbose]
23
+ Expressir.configuration.benchmark_save = options[:save]
24
+ Expressir.configuration.benchmark_format = options[:format] if options[:format]
25
+ end
26
+
27
+ def benchmark_cache_file(path, cache_path)
28
+ say "Express Schema Loading Benchmark with Caching"
29
+ say "--------------------------------"
30
+ say "Schema: #{path}"
31
+ say "Cache: #{cache_path}"
32
+ say "--------------------------------"
33
+
34
+ # Benchmark with caching
35
+ results = Expressir::Benchmark.measure_with_cache(path, cache_path) do |file|
36
+ Expressir::Express::Parser.from_file(file)
37
+ end
38
+
39
+ if results[:repository]
40
+ schema_count = results[:repository].schemas.size
41
+ say "Loaded #{schema_count} schemas"
42
+ else
43
+ say "Failed to load schema"
44
+ end
45
+ end
46
+
47
+ def benchmark_cache_from_yaml(yaml_path, cache_path)
48
+ say "Express Schema Loading Benchmark with Caching from YAML"
49
+ say "--------------------------------"
50
+
51
+ # Load schema list from YAML
52
+ schema_list = YAML.load_file(yaml_path)
53
+ if schema_list.is_a?(Hash) && schema_list["schemas"]
54
+ # Handle format: { "schemas": ["path1", "path2", ...] }
55
+ schema_files = schema_list["schemas"]
56
+ elsif schema_list.is_a?(Array)
57
+ # Handle format: ["path1", "path2", ...]
58
+ schema_files = schema_list
59
+ else
60
+ say "Invalid YAML format. Expected an array of schema paths or a hash with a 'schemas' key."
61
+ return
62
+ end
63
+
64
+ say "YAML File: #{yaml_path}"
65
+ say "Number of schemas in list: #{schema_files.size}"
66
+ say "Cache: #{cache_path}"
67
+ say "--------------------------------"
68
+
69
+ # Process each file with caching
70
+ schema_files.each_with_index do |file, index|
71
+ say "Processing file #{index + 1}/#{schema_files.size}: #{file}"
72
+
73
+ # Benchmark with caching
74
+ Expressir::Benchmark.measure_with_cache(file, cache_path) do |path|
75
+ Expressir::Express::Parser.from_file(path)
76
+ end
77
+
78
+ say "--------------------------------"
79
+ end
80
+ end
81
+
82
+ def generate_temp_cache_path
83
+ require "tempfile"
84
+ Tempfile.new(["expressir_cache", ".bin"]).path
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,20 @@
1
+ module Expressir
2
+ module Commands
3
+ class Clean < Base
4
+ def run(path)
5
+ repository = Expressir::Express::Parser.from_file(path)
6
+ formatted_schemas = repository.schemas.map do |schema|
7
+ # Format schema without remarks
8
+ schema.to_s(no_remarks: true)
9
+ end.join("\n\n")
10
+
11
+ if options[:output]
12
+ File.write(options[:output], formatted_schemas)
13
+ say "Cleaned schema written to #{options[:output]}"
14
+ else
15
+ say formatted_schemas
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,307 @@
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
+ say JSON.pretty_generate(build_structured_report(reports))
279
+ end
280
+
281
+ def display_yaml_output(reports)
282
+ say build_structured_report(reports).to_yaml
283
+ end
284
+
285
+ # Parse and validate the skip_types option
286
+ # @return [Array<String>] Array of validated entity type names
287
+ def parse_skip_types
288
+ skip_types_option = options["exclude"]
289
+ return [] unless skip_types_option
290
+
291
+ # Split by comma and clean up whitespace
292
+ requested_types = skip_types_option.split(",").map(&:strip).map(&:upcase)
293
+
294
+ # Validate against known entity types
295
+ valid_types = Expressir::Coverage::ENTITY_TYPE_MAP.keys
296
+ invalid_types = requested_types - valid_types
297
+
298
+ unless invalid_types.empty?
299
+ exit_with_error "Invalid entity types: #{invalid_types.join(', ')}. " \
300
+ "Valid types are: #{valid_types.join(', ')}"
301
+ end
302
+
303
+ requested_types
304
+ end
305
+ end
306
+ end
307
+ 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