suma 0.3.0 → 0.4.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 +4 -4
- data/.gitignore +2 -5
- data/README.adoc +70 -12
- data/lib/suma/cli/check_svg_quality.rb +82 -91
- data/lib/suma/cli/export.rb +37 -19
- data/lib/suma/cli/reformat.rb +39 -63
- data/lib/suma/cli/validate.rb +14 -5
- data/lib/suma/cli/validate_links.rb +11 -155
- data/lib/suma/express_reformatter.rb +94 -0
- data/lib/suma/link_validation.rb +144 -0
- data/lib/suma/link_validator.rb +2 -1
- data/lib/suma/schema_collection.rb +1 -1
- data/lib/suma/schema_exporter.rb +14 -26
- data/lib/suma/svg_quality/scanner.rb +61 -0
- data/lib/suma/svg_quality.rb +1 -0
- data/lib/suma/term_classification.rb +78 -0
- data/lib/suma/term_extractor.rb +16 -45
- data/lib/suma/version.rb +1 -1
- data/lib/suma.rb +4 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 304c4c7a2ec03c83604a127370259b022e0102d303c4951c2d730d3681d3ad39
|
|
4
|
+
data.tar.gz: b8d9bcd807ea0aadb4f038dd58823d829d495827e4785316cb7c715e79b512cb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 21977b339cd0a16936940c50de766ba2130f3dc46df7965db1de04360a65c1d685e1b59786e9a49548d0e79f1d20befcf732a66482b6e8b8793a547bd557d28e
|
|
7
|
+
data.tar.gz: bd1122e6d73a9a22b3a3830c7cf654c909a541e7c332808349b15bb469f772338ad7a84a25590f1c8d13b46b84d8399673fa2f43f16f5d943e0e1f8e2469a08a
|
data/.gitignore
CHANGED
|
@@ -13,11 +13,8 @@
|
|
|
13
13
|
.ruby-version
|
|
14
14
|
Gemfile.lock
|
|
15
15
|
|
|
16
|
-
# local planning scratch directories (not for commit)
|
|
17
|
-
/TODO
|
|
18
|
-
/TODO.cleanup/
|
|
19
|
-
/TODO.refactor/
|
|
20
|
-
/TODO.remaining/
|
|
16
|
+
# local planning scratch directories and files (not for commit)
|
|
17
|
+
/TODO*
|
|
21
18
|
|
|
22
19
|
# vendored gems (use Gemfile / Gemfile.lock elsewhere)
|
|
23
20
|
/*.gem
|
data/README.adoc
CHANGED
|
@@ -1065,34 +1065,31 @@ examples demonstrate common usage patterns.
|
|
|
1065
1065
|
require 'suma'
|
|
1066
1066
|
|
|
1067
1067
|
# Build a collection with default settings
|
|
1068
|
-
Suma::Processor.
|
|
1068
|
+
Suma::Processor.new(
|
|
1069
1069
|
metanorma_yaml_path: "metanorma-srl.yml",
|
|
1070
1070
|
schemas_all_path: "schemas-srl.yml",
|
|
1071
1071
|
compile: true,
|
|
1072
1072
|
output_directory: "_site"
|
|
1073
|
-
)
|
|
1073
|
+
).run
|
|
1074
1074
|
|
|
1075
1075
|
# Generate schema listing without compilation
|
|
1076
|
-
Suma::Processor.
|
|
1076
|
+
Suma::Processor.new(
|
|
1077
1077
|
metanorma_yaml_path: "metanorma-srl.yml",
|
|
1078
1078
|
schemas_all_path: "schemas-srl.yml",
|
|
1079
|
-
compile: false
|
|
1080
|
-
|
|
1081
|
-
)
|
|
1079
|
+
compile: false
|
|
1080
|
+
).run
|
|
1082
1081
|
----
|
|
1083
1082
|
|
|
1084
|
-
=== Working with schema
|
|
1083
|
+
=== Working with schema manifests
|
|
1084
|
+
|
|
1085
|
+
Schema manifests are loaded via `Expressir::SchemaManifest`:
|
|
1085
1086
|
|
|
1086
1087
|
[source,ruby]
|
|
1087
1088
|
----
|
|
1088
1089
|
require 'suma'
|
|
1089
1090
|
|
|
1090
|
-
# Load schemas using SchemaConfig
|
|
1091
1091
|
schemas_file_path = "schemas-srl.yml"
|
|
1092
|
-
schemas_config =
|
|
1093
|
-
|
|
1094
|
-
# Set the initial path to resolve relative paths
|
|
1095
|
-
schemas_config.set_initial_path(schemas_file_path)
|
|
1092
|
+
schemas_config = Expressir::SchemaManifest.from_file(schemas_file_path)
|
|
1096
1093
|
|
|
1097
1094
|
# Access schema information
|
|
1098
1095
|
schemas_config.schemas.each do |schema|
|
|
@@ -1101,6 +1098,67 @@ schemas_config.schemas.each do |schema|
|
|
|
1101
1098
|
end
|
|
1102
1099
|
----
|
|
1103
1100
|
|
|
1101
|
+
=== Extracting terms
|
|
1102
|
+
|
|
1103
|
+
[source,ruby]
|
|
1104
|
+
----
|
|
1105
|
+
require 'suma'
|
|
1106
|
+
|
|
1107
|
+
Suma::TermExtractor.new(
|
|
1108
|
+
"schemas-smrl-part-2.yml",
|
|
1109
|
+
"glossarist_output",
|
|
1110
|
+
urn: "urn:iso:std:iso:10303:-2:ed-2:en:tech:*",
|
|
1111
|
+
).call
|
|
1112
|
+
----
|
|
1113
|
+
|
|
1114
|
+
=== Generating a register.yaml
|
|
1115
|
+
|
|
1116
|
+
[source,ruby]
|
|
1117
|
+
----
|
|
1118
|
+
require 'suma'
|
|
1119
|
+
|
|
1120
|
+
Suma::RegisterManifestGenerator.new(
|
|
1121
|
+
"schemas-smrl-part-2.yml",
|
|
1122
|
+
"register_output",
|
|
1123
|
+
urn: "urn:iso:std:iso:10303:-2:ed-2:en:tech:*",
|
|
1124
|
+
id: "iso10303-2-express",
|
|
1125
|
+
ref: "ISO 10303-2 EXPRESS Concepts",
|
|
1126
|
+
).generate
|
|
1127
|
+
----
|
|
1128
|
+
|
|
1129
|
+
=== Architecture
|
|
1130
|
+
|
|
1131
|
+
Suma's domain is split into MECE concerns:
|
|
1132
|
+
|
|
1133
|
+
* **Data models** (pure): `CollectionManifest`, `ExpressSchema`
|
|
1134
|
+
* **Value objects** (pure): `SchemaCategory`, `Urn`
|
|
1135
|
+
* **Services** (single concern): `ManifestTraverser`, `SchemaDiscovery`,
|
|
1136
|
+
`SchemaCompiler`, `SchemaExporter`, `SchemaIndex`, `LinkValidator`,
|
|
1137
|
+
`SchemaComparer`, `SchemaManifestGenerator`, `TermExtractor`,
|
|
1138
|
+
`RegisterManifestGenerator`, `Processor`
|
|
1139
|
+
* **Renderers** (pure, composable): `SchemaTemplate::Plain`,
|
|
1140
|
+
`SchemaTemplate::Document`
|
|
1141
|
+
|
|
1142
|
+
Key invariants:
|
|
1143
|
+
|
|
1144
|
+
* The classifier `ExpressSchema::Type.classify(id:, path:)` is the single
|
|
1145
|
+
source of truth for schema type. `SchemaCategory`, `SchemaNaming`,
|
|
1146
|
+
`SchemaExporter`, and `RegisterManifestGenerator` all derive from it.
|
|
1147
|
+
* `Urn` owns URN semantics (wildcard stripping, base/alias split, leaf
|
|
1148
|
+
composition).
|
|
1149
|
+
* `CollectionManifest` is a pure data model; tree walking lives in
|
|
1150
|
+
`ManifestTraverser`, schema I/O lives in `SchemaDiscovery`.
|
|
1151
|
+
* `SchemaCompiler` orchestrates one Metanorma compilation; the adoc body
|
|
1152
|
+
is injected via a `SchemaTemplate::*` renderer.
|
|
1153
|
+
|
|
1154
|
+
Code-style rules enforced throughout `lib/suma/`:
|
|
1155
|
+
|
|
1156
|
+
* Ruby `autoload` only — no `require_relative`
|
|
1157
|
+
* No `send` to call private methods
|
|
1158
|
+
* No `instance_variable_set` / `instance_variable_get`
|
|
1159
|
+
* No `respond_to?` for type checking
|
|
1160
|
+
* `Utils.log` writes to `$stderr`; debug level gated by `SUMA_DEBUG`
|
|
1161
|
+
|
|
1104
1162
|
|
|
1105
1163
|
== Copyright and license
|
|
1106
1164
|
|
|
@@ -4,16 +4,19 @@ require "pathname"
|
|
|
4
4
|
|
|
5
5
|
module Suma
|
|
6
6
|
module Cli
|
|
7
|
-
# Check SVG quality
|
|
7
|
+
# Check SVG quality. Thin adapter around +Suma::SvgQuality::Scanner+:
|
|
8
|
+
# argument parsing, file discovery, sorting, filtering, output
|
|
9
|
+
# formatting. The deep scanner module owns validation orchestration
|
|
10
|
+
# and is reachable from specs without invoking Thor.
|
|
8
11
|
class CheckSvgQuality
|
|
9
12
|
DATA_PATH = "schemas"
|
|
10
13
|
DEFAULT_PATTERN = "**/*.svg"
|
|
11
14
|
DEFAULT_PROFILE = :metanorma
|
|
12
15
|
|
|
13
16
|
def initialize(pattern: DEFAULT_PATTERN, profile: DEFAULT_PROFILE,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
format: "terminal", output: nil, min_errors: nil,
|
|
18
|
+
summary_only: false, progress: false, limit: nil,
|
|
19
|
+
sort: "errors")
|
|
17
20
|
@options = {
|
|
18
21
|
pattern: pattern,
|
|
19
22
|
profile: profile,
|
|
@@ -31,71 +34,42 @@ module Suma
|
|
|
31
34
|
require "svg_conform"
|
|
32
35
|
|
|
33
36
|
path_obj = Pathname.new(path).expand_path
|
|
34
|
-
|
|
35
|
-
# Enable progress by default when outputting to terminal
|
|
36
37
|
show_progress = options[:progress] || ($stdout.tty? && !options[:output])
|
|
37
|
-
if show_progress
|
|
38
|
-
$stdout.sync = true
|
|
39
|
-
$stderr.sync = true
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
if path_obj.file?
|
|
43
|
-
# Single file mode - show detailed errors
|
|
44
|
-
analyze_single_file(path_obj)
|
|
45
|
-
else
|
|
46
|
-
# Directory mode - show batch report
|
|
47
|
-
svg_files = find_svg_files(path_obj)
|
|
48
|
-
|
|
49
|
-
if svg_files.empty?
|
|
50
|
-
puts "No SVG files found in #{path}"
|
|
51
|
-
return
|
|
52
|
-
end
|
|
38
|
+
sync_stdio! if show_progress
|
|
53
39
|
|
|
54
|
-
|
|
55
|
-
|
|
40
|
+
files = discover_files(path_obj)
|
|
41
|
+
return if files.empty?
|
|
56
42
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
43
|
+
if single_file?(path_obj, files)
|
|
44
|
+
print_single_report(scan_single(files.first))
|
|
45
|
+
else
|
|
46
|
+
scan_and_output(files, show_progress)
|
|
61
47
|
end
|
|
62
48
|
end
|
|
63
49
|
|
|
64
|
-
|
|
65
|
-
validator = SvgConform::Validator.new
|
|
66
|
-
result = validator.validate_file(path.to_s, profile: options[:profile])
|
|
67
|
-
|
|
68
|
-
puts "📄 SVG Quality Report: #{path}"
|
|
69
|
-
puts ""
|
|
70
|
-
puts " Valid: #{result.valid? ? 'YES ✅' : 'NO ❌'}"
|
|
71
|
-
puts " Errors: #{result.error_count}"
|
|
72
|
-
puts ""
|
|
50
|
+
private
|
|
73
51
|
|
|
74
|
-
|
|
75
|
-
puts " 📋 Error Details"
|
|
76
|
-
puts ""
|
|
52
|
+
attr_reader :options
|
|
77
53
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
by_req.each do |req_id, errors|
|
|
82
|
-
puts " #{req_id} (#{errors.size} occurrences)"
|
|
83
|
-
errors.first(5).each do |e|
|
|
84
|
-
puts " - #{e.message}"
|
|
85
|
-
end
|
|
86
|
-
if errors.size > 5
|
|
87
|
-
puts " ... and #{errors.size - 5} more"
|
|
88
|
-
end
|
|
89
|
-
puts ""
|
|
90
|
-
end
|
|
91
|
-
end
|
|
54
|
+
def single_file?(path_obj, files)
|
|
55
|
+
path_obj.file? && files.size == 1
|
|
92
56
|
end
|
|
93
57
|
|
|
94
|
-
|
|
58
|
+
def scan_and_output(files, show_progress)
|
|
59
|
+
puts "🔍 Scanning #{files.size} SVG files..." if show_progress
|
|
60
|
+
batch = SvgQuality::Scanner.new(
|
|
61
|
+
profile: options[:profile],
|
|
62
|
+
progress: progress_adapter(show_progress),
|
|
63
|
+
).scan(files)
|
|
64
|
+
output_report(sort_report(batch))
|
|
65
|
+
end
|
|
95
66
|
|
|
96
|
-
|
|
67
|
+
def sync_stdio!
|
|
68
|
+
$stdout.sync = true
|
|
69
|
+
$stderr.sync = true
|
|
70
|
+
end
|
|
97
71
|
|
|
98
|
-
def
|
|
72
|
+
def discover_files(path)
|
|
99
73
|
if path.directory?
|
|
100
74
|
Pathname.glob(path.join(options[:pattern])).select(&:file?)
|
|
101
75
|
elsif path.file? && path.extname == ".svg"
|
|
@@ -105,34 +79,47 @@ module Suma
|
|
|
105
79
|
end
|
|
106
80
|
end
|
|
107
81
|
|
|
108
|
-
def
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
82
|
+
def scan_single(path)
|
|
83
|
+
SvgQuality::Scanner.new(profile: options[:profile]).scan_file(path)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def print_single_report(report)
|
|
87
|
+
result = report
|
|
88
|
+
puts "📄 SVG Quality Report: #{result.file_path}"
|
|
89
|
+
puts ""
|
|
90
|
+
puts " Valid: #{result.valid? ? 'YES ✅' : 'NO ❌'}"
|
|
91
|
+
puts " Errors: #{result.error_count}"
|
|
92
|
+
puts ""
|
|
93
|
+
|
|
94
|
+
return unless result.errors.any?
|
|
95
|
+
|
|
96
|
+
puts " 📋 Error Details"
|
|
97
|
+
puts ""
|
|
98
|
+
by_req = result.errors.group_by(&:requirement_id)
|
|
99
|
+
by_req.each do |req_id, errors|
|
|
100
|
+
puts " #{req_id} (#{errors.size} occurrences)"
|
|
101
|
+
errors.first(5).each { |e| puts " - #{e.message}" }
|
|
102
|
+
puts " ... and #{errors.size - 5} more" if errors.size > 5
|
|
103
|
+
puts ""
|
|
125
104
|
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def progress_adapter(enabled)
|
|
108
|
+
return SvgQuality::Scanner::NullProgress.new unless enabled
|
|
126
109
|
|
|
127
|
-
|
|
110
|
+
->(_index, _total, report) do
|
|
111
|
+
tier = report.quality_tier
|
|
112
|
+
status = report.valid? ? "✅" : "❌"
|
|
113
|
+
$stderr.print " #{tier[:emoji]} #{report.error_count} errors " \
|
|
114
|
+
"#{status} #{shorten_path(report.file_path)}\n"
|
|
115
|
+
$stderr.flush
|
|
116
|
+
end
|
|
128
117
|
end
|
|
129
118
|
|
|
130
119
|
def sort_report(batch_report)
|
|
131
120
|
case options[:sort]
|
|
132
|
-
when :quality
|
|
133
|
-
|
|
134
|
-
else
|
|
135
|
-
batch_report.sort_by_errors
|
|
121
|
+
when :quality then batch_report.sort_by_quality
|
|
122
|
+
else batch_report.sort_by_errors
|
|
136
123
|
end
|
|
137
124
|
end
|
|
138
125
|
|
|
@@ -140,21 +127,25 @@ module Suma
|
|
|
140
127
|
filtered = batch_report.filter_by_min_errors(options[:min_errors])
|
|
141
128
|
limited = filtered.limit(options[:limit])
|
|
142
129
|
|
|
143
|
-
formatter =
|
|
144
|
-
when :json
|
|
145
|
-
SvgQuality::Formatters::JsonFormatter.new(limited,
|
|
146
|
-
output: options[:output])
|
|
147
|
-
when :yaml
|
|
148
|
-
SvgQuality::Formatters::YamlFormatter.new(limited,
|
|
149
|
-
output: options[:output])
|
|
150
|
-
else
|
|
151
|
-
SvgQuality::Formatters::TerminalFormatter.new(limited,
|
|
152
|
-
output: options[:output], sort: options[:sort])
|
|
153
|
-
end
|
|
154
|
-
|
|
130
|
+
formatter = formatter_for(limited)
|
|
155
131
|
puts formatter.format
|
|
156
132
|
end
|
|
157
133
|
|
|
134
|
+
def formatter_for(batch_report)
|
|
135
|
+
case options[:format].to_sym
|
|
136
|
+
when :json
|
|
137
|
+
SvgQuality::Formatters::JsonFormatter.new(batch_report,
|
|
138
|
+
output: options[:output])
|
|
139
|
+
when :yaml
|
|
140
|
+
SvgQuality::Formatters::YamlFormatter.new(batch_report,
|
|
141
|
+
output: options[:output])
|
|
142
|
+
else
|
|
143
|
+
SvgQuality::Formatters::TerminalFormatter.new(batch_report,
|
|
144
|
+
output: options[:output],
|
|
145
|
+
sort: options[:sort])
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
158
149
|
def shorten_path(path)
|
|
159
150
|
p = Pathname.new(path)
|
|
160
151
|
if p.absolute?
|
data/lib/suma/cli/export.rb
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "thor"
|
|
4
|
+
require "pathname"
|
|
4
5
|
|
|
5
6
|
module Suma
|
|
6
7
|
module Cli
|
|
7
|
-
# Export command
|
|
8
|
+
# Export command. Thin Thor adapter that constructs
|
|
9
|
+
# +Suma::ExpressSchema+ instances from manifest entries or
|
|
10
|
+
# standalone +.exp+ files, then delegates the actual writing to
|
|
11
|
+
# +Suma::SchemaExporter+.
|
|
12
|
+
#
|
|
13
|
+
# The schema-type → output-subdirectory mapping lives in
|
|
14
|
+
# +Suma::SchemaCategory+, the single source of truth. The exporter
|
|
15
|
+
# itself never classifies — it consumes loaded ExpressSchema
|
|
16
|
+
# objects whose output paths were set by this adapter.
|
|
8
17
|
class Export < Thor
|
|
9
18
|
desc "export *FILES",
|
|
10
19
|
"Export EXPRESS schemas from manifest files or " \
|
|
@@ -38,7 +47,7 @@ module Suma
|
|
|
38
47
|
end
|
|
39
48
|
|
|
40
49
|
def run(files, options)
|
|
41
|
-
schemas =
|
|
50
|
+
schemas = files.flat_map { |file| build_schemas(file) }
|
|
42
51
|
|
|
43
52
|
exporter = SchemaExporter.new(
|
|
44
53
|
schemas: schemas,
|
|
@@ -52,22 +61,12 @@ module Suma
|
|
|
52
61
|
exporter.export
|
|
53
62
|
end
|
|
54
63
|
|
|
55
|
-
def
|
|
56
|
-
all_schemas = []
|
|
57
|
-
|
|
58
|
-
files.each do |file|
|
|
59
|
-
all_schemas += process_file(file)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
all_schemas
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def process_file(file)
|
|
64
|
+
def build_schemas(file)
|
|
66
65
|
case File.extname(file).downcase
|
|
67
66
|
when ".yml", ".yaml"
|
|
68
|
-
|
|
67
|
+
build_from_manifest(file)
|
|
69
68
|
when ".exp"
|
|
70
|
-
[
|
|
69
|
+
[build_standalone(file)]
|
|
71
70
|
else
|
|
72
71
|
raise ArgumentError, "Unsupported file type: #{file}. " \
|
|
73
72
|
"Only .yml, .yaml, and .exp files are " \
|
|
@@ -75,13 +74,32 @@ module Suma
|
|
|
75
74
|
end
|
|
76
75
|
end
|
|
77
76
|
|
|
78
|
-
def
|
|
77
|
+
def build_from_manifest(file)
|
|
79
78
|
manifest = Expressir::SchemaManifest.from_file(file)
|
|
80
|
-
manifest.schemas
|
|
79
|
+
manifest.schemas.map { |entry| build_from_manifest_entry(entry) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def build_from_manifest_entry(entry)
|
|
83
|
+
category = SchemaCategory.for_schema(id: entry.id, path: entry.path)
|
|
84
|
+
ExpressSchema.new(
|
|
85
|
+
id: entry.id,
|
|
86
|
+
path: entry.path.to_s,
|
|
87
|
+
output_path: output_root.join(category.directory).to_s,
|
|
88
|
+
is_standalone_file: false,
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def build_standalone(exp_file)
|
|
93
|
+
ExpressSchema.new(
|
|
94
|
+
id: nil,
|
|
95
|
+
path: File.expand_path(exp_file),
|
|
96
|
+
output_path: output_root.to_s,
|
|
97
|
+
is_standalone_file: true,
|
|
98
|
+
)
|
|
81
99
|
end
|
|
82
100
|
|
|
83
|
-
def
|
|
84
|
-
|
|
101
|
+
def output_root
|
|
102
|
+
Pathname.new(options[:output]).expand_path
|
|
85
103
|
end
|
|
86
104
|
|
|
87
105
|
def self.exit_on_failure?
|
data/lib/suma/cli/reformat.rb
CHANGED
|
@@ -4,84 +4,60 @@ require "thor"
|
|
|
4
4
|
|
|
5
5
|
module Suma
|
|
6
6
|
module Cli
|
|
7
|
-
# Reformat command for reformatting EXPRESS files
|
|
7
|
+
# Reformat command for reformatting EXPRESS files.
|
|
8
|
+
#
|
|
9
|
+
# Thin Thor adapter around Suma::ExpressReformatter: argument
|
|
10
|
+
# parsing, file discovery, and read/transform/write. The content
|
|
11
|
+
# transformation itself lives in the deep module and is tested
|
|
12
|
+
# independently.
|
|
8
13
|
class Reformat < Thor
|
|
9
|
-
desc "reformat EXPRESS_FILE_PATH",
|
|
10
|
-
"Reformat EXPRESS files"
|
|
14
|
+
desc "reformat EXPRESS_FILE_PATH", "Reformat EXPRESS files"
|
|
11
15
|
option :recursive, type: :boolean, default: false, aliases: "-r",
|
|
12
16
|
desc: "Reformat EXPRESS files under the specified " \
|
|
13
17
|
"path recursively"
|
|
14
18
|
|
|
15
|
-
def reformat(express_file_path)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"`#{express_file_path}` not found."
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
if File.extname(express_file_path) != ".exp"
|
|
23
|
-
raise ArgumentError, "Specified file `#{express_file_path}` is " \
|
|
24
|
-
"not an EXPRESS file."
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
exp_files = [express_file_path]
|
|
28
|
-
elsif options[:recursive]
|
|
29
|
-
exp_files = Dir.glob("#{express_file_path}/**/*.exp")
|
|
30
|
-
else
|
|
31
|
-
exp_files = Dir.glob("#{express_file_path}/*.exp")
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
if exp_files.empty?
|
|
35
|
-
raise Errno::ENOENT, "No EXPRESS files found in " \
|
|
36
|
-
"`#{express_file_path}`."
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
run(exp_files)
|
|
19
|
+
def reformat(express_file_path)
|
|
20
|
+
files = discover_files(express_file_path)
|
|
21
|
+
ensure_files_found!(files, express_file_path)
|
|
22
|
+
process_files(files)
|
|
40
23
|
end
|
|
41
24
|
|
|
42
25
|
private
|
|
43
26
|
|
|
44
|
-
def
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
end
|
|
48
|
-
end
|
|
27
|
+
def discover_files(path)
|
|
28
|
+
return [path] if File.file?(path)
|
|
29
|
+
return Dir.glob("#{path}/**/*.exp") if options[:recursive]
|
|
49
30
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
file_content = File.read(file)
|
|
53
|
-
|
|
54
|
-
# Extract all comments between '(*"' and '\n*)'
|
|
55
|
-
# Avoid incorrect selection of some comment blocks
|
|
56
|
-
# containing '(*text*)' inside
|
|
57
|
-
comments = file_content.scan(/\(\*"(.*?)\n\*\)/m).map(&:first)
|
|
58
|
-
|
|
59
|
-
if comments.any?
|
|
60
|
-
content_without_comments = file_content.gsub(/\(\*".*?\n\*\)/m, "")
|
|
61
|
-
|
|
62
|
-
# remove extra newlines
|
|
63
|
-
new_content = content_without_comments.gsub(/(\n\n+)/, "\n\n")
|
|
64
|
-
# Add '(*"' and '\n*)' to enclose the comment block
|
|
65
|
-
new_comments = comments.map { |c| "(*\"#{c}\n*)" }.join("\n\n")
|
|
66
|
-
# Append the comments to the end of the file
|
|
67
|
-
new_content = "#{new_content}\n\n#{new_comments}\n"
|
|
31
|
+
Dir.glob("#{path}/*.exp")
|
|
32
|
+
end
|
|
68
33
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return
|
|
34
|
+
def ensure_files_found!(files, path)
|
|
35
|
+
if File.file?(path)
|
|
36
|
+
unless File.extname(path) == ".exp"
|
|
37
|
+
raise ArgumentError,
|
|
38
|
+
"Specified file `#{path}` is not an EXPRESS file."
|
|
75
39
|
end
|
|
76
|
-
|
|
77
|
-
|
|
40
|
+
elsif !File.exist?(path)
|
|
41
|
+
raise Errno::ENOENT,
|
|
42
|
+
"Specified EXPRESS file `#{path}` not found."
|
|
78
43
|
end
|
|
44
|
+
return unless files.empty?
|
|
45
|
+
|
|
46
|
+
raise Errno::ENOENT, "No EXPRESS files found in `#{path}`."
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def process_files(files)
|
|
50
|
+
files.each { |file| reformat_one(file) }
|
|
79
51
|
end
|
|
80
52
|
|
|
81
|
-
def
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
53
|
+
def reformat_one(file)
|
|
54
|
+
result = ExpressReformatter.call(File.read(file))
|
|
55
|
+
if result.changed?
|
|
56
|
+
File.write(file, result.content)
|
|
57
|
+
puts "Reformatted EXPRESS file and saved to #{file}"
|
|
58
|
+
else
|
|
59
|
+
puts "No changes made to #{file}"
|
|
60
|
+
end
|
|
85
61
|
end
|
|
86
62
|
end
|
|
87
63
|
end
|
data/lib/suma/cli/validate.rb
CHANGED
|
@@ -4,14 +4,23 @@ require "thor"
|
|
|
4
4
|
|
|
5
5
|
module Suma
|
|
6
6
|
module Cli
|
|
7
|
-
#
|
|
7
|
+
# Validate command group. Thin Thor adapter around
|
|
8
|
+
# +Suma::LinkValidation+ — argument parsing, result presentation.
|
|
9
|
+
# All orchestration (manifest loading, link extraction, schema
|
|
10
|
+
# indexing, validation) lives in the deep module and is reachable
|
|
11
|
+
# from specs without invoking Thor.
|
|
8
12
|
class Validate < Thor
|
|
9
13
|
desc "links SCHEMAS_FILE DOCUMENTS_PATH [OUTPUT_FILE]",
|
|
10
14
|
"Extract and validate express links without creating intermediate file"
|
|
11
|
-
def links(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
def links(schemas_file = "schemas-srl.yml",
|
|
16
|
+
documents_path = "documents",
|
|
17
|
+
output_file = "validation_results.txt")
|
|
18
|
+
result = LinkValidation.new(
|
|
19
|
+
schemas_file: schemas_file,
|
|
20
|
+
documents_path: documents_path,
|
|
21
|
+
output_file: output_file,
|
|
22
|
+
).call
|
|
23
|
+
puts LinkValidation.generate_summary(result)
|
|
15
24
|
end
|
|
16
25
|
end
|
|
17
26
|
end
|