suma 0.1.8 → 0.1.9

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc14e31d38fd5c02cf635587a9b69065ca52c6c96eefc28b6176253e328572e8
4
- data.tar.gz: 15b6aa0f50e227bd70d21fd615d036befdac46466186ef53cdb71c2e53b03ab7
3
+ metadata.gz: 52f25d69f23dba6c3d0c420c241279354bc8e838ac1f1e58d32e1392408da68e
4
+ data.tar.gz: 5cfa1b2e128694c04c75a0ca3eda31be63c90e2c5759ee564230525529edf07c
5
5
  SHA512:
6
- metadata.gz: 7d0b42b4e0bd3d3c777f869b181fa9ce13b02ed7d05cf691da75546406bc005c2c6ded60c68360048659d2102486029f5ccfd3bc648eff5d8eea43e89778f4c5
7
- data.tar.gz: 89665aa02f15bba0314fa27850664bbf0483473dfdecabc2a69a408084861da6aade7e6f10b11f90a8bca06066690c04e7fd4a289d9c6e9f1ce7650e3e62c757
6
+ metadata.gz: 7db5c9725523372171efb9d4234cbf9e7819c0ac0093128e92dcaa737cf34653d33573df97816c9253645d0f6abce7e9e96d4ca503f787336ec4dd3824641db8
7
+ data.tar.gz: 8095d98452ac83ab609466df69ee064b9202342d9dfd1842262a2edf571002b2e76f1855b47582cda228f7f321904b1927a46d6b14b7ff2318f0e11cf4e2afa5
data/.rubocop.yml CHANGED
@@ -1,11 +1,14 @@
1
- # Auto-generated by Cimas: Do not edit it manually!
2
- # See https://github.com/metanorma/cimas
3
1
  inherit_from:
4
- - .rubocop_todo.yml
5
2
  - https://raw.githubusercontent.com/riboseinc/oss-guides/master/ci/rubocop.yml
3
+ - .rubocop_todo.yml
6
4
 
7
- # local repo-specific modifications
8
- # ...
5
+ require:
6
+ - rubocop-performance
7
+ - rubocop-rake
8
+ - rubocop-rspec
9
9
 
10
10
  AllCops:
11
- TargetRubyVersion: 3.4
11
+ TargetRubyVersion: 3.0
12
+ NewCops: enable
13
+ Exclude:
14
+ - 'vendor/**/*'
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2024-10-17 02:50:55 UTC using RuboCop version 1.67.0.
3
+ # on 2025-03-21 11:43:08 UTC using RuboCop version 1.74.0.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
@@ -13,20 +13,56 @@ Gemspec/DuplicatedAssignment:
13
13
  Exclude:
14
14
  - 'suma.gemspec'
15
15
 
16
+ # Offense count: 2
17
+ # This cop supports safe autocorrection (--autocorrect).
18
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
19
+ # SupportedStyles: outdent, indent
20
+ Layout/AccessModifierIndentation:
21
+ Exclude:
22
+ - 'lib/suma/cli/build.rb'
23
+ - 'lib/suma/cli/links.rb'
24
+
16
25
  # Offense count: 1
17
- # Configuration parameters: Severity, Include.
18
- # Include: **/*.gemspec
19
- Gemspec/RequiredRubyVersion:
26
+ # This cop supports safe autocorrection (--autocorrect).
27
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
28
+ # SupportedStyles: with_first_argument, with_fixed_indentation
29
+ Layout/ArgumentAlignment:
20
30
  Exclude:
21
- - 'suma.gemspec'
31
+ - 'lib/suma/cli/links.rb'
32
+
33
+ # Offense count: 19
34
+ # This cop supports safe autocorrection (--autocorrect).
35
+ # Configuration parameters: AllowForAlignment.
36
+ Layout/CommentIndentation:
37
+ Exclude:
38
+ - 'lib/suma/cli/build.rb'
39
+ - 'lib/suma/cli/links.rb'
22
40
 
23
- # Offense count: 21
41
+ # Offense count: 1
42
+ # This cop supports safe autocorrection (--autocorrect).
43
+ # Configuration parameters: EnforcedStyleAlignWith, Severity.
44
+ # SupportedStylesAlignWith: keyword, variable, start_of_line
45
+ Layout/EndAlignment:
46
+ Exclude:
47
+ - 'lib/suma/cli/build.rb'
48
+
49
+ # Offense count: 28
24
50
  # This cop supports safe autocorrection (--autocorrect).
25
- # Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns.
51
+ # Configuration parameters: Width, AllowedPatterns.
52
+ Layout/IndentationWidth:
53
+ Exclude:
54
+ - 'lib/suma/cli/build.rb'
55
+ - 'lib/suma/cli/links.rb'
56
+
57
+ # Offense count: 43
58
+ # This cop supports safe autocorrection (--autocorrect).
59
+ # Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
26
60
  # URISchemes: http, https
27
61
  Layout/LineLength:
28
62
  Exclude:
29
63
  - 'lib/suma/cli.rb'
64
+ - 'lib/suma/cli/build.rb'
65
+ - 'lib/suma/cli/links.rb'
30
66
  - 'lib/suma/collection_manifest.rb'
31
67
  - 'lib/suma/processor.rb'
32
68
  - 'lib/suma/schema_attachment.rb'
@@ -35,43 +71,78 @@ Layout/LineLength:
35
71
  - 'lib/suma/thor_ext.rb'
36
72
  - 'suma.gemspec'
37
73
 
74
+ # Offense count: 1
75
+ # This cop supports safe autocorrection (--autocorrect).
76
+ # Configuration parameters: AllowInHeredoc.
77
+ Layout/TrailingWhitespace:
78
+ Exclude:
79
+ - 'lib/suma/cli/links.rb'
80
+
38
81
  # Offense count: 1
39
82
  Lint/DuplicateMethods:
40
83
  Exclude:
41
84
  - 'lib/suma/express_schema.rb'
42
85
 
43
- # Offense count: 8
86
+ # Offense count: 11
44
87
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
45
88
  Metrics/AbcSize:
46
89
  Exclude:
47
- - 'lib/suma/collection_manifest.rb'
48
- - 'lib/suma/processor.rb'
90
+ - 'lib/suma/cli/links.rb'
49
91
  - 'lib/suma/schema_attachment.rb'
50
- - 'lib/suma/schema_collection.rb'
51
- - 'lib/suma/schema_config/config.rb'
52
92
  - 'lib/suma/schema_document.rb'
53
93
  - 'lib/suma/thor_ext.rb'
54
94
 
55
95
  # Offense count: 1
56
- # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
96
+ # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode.
57
97
  # AllowedMethods: refine
58
98
  Metrics/BlockLength:
59
- Max: 33
99
+ Max: 64
60
100
 
61
101
  # Offense count: 2
62
102
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
63
103
  Metrics/CyclomaticComplexity:
64
104
  Exclude:
65
- - 'lib/suma/collection_manifest.rb'
105
+ - 'lib/suma/cli/links.rb'
66
106
  - 'lib/suma/thor_ext.rb'
67
107
 
68
- # Offense count: 7
108
+ # Offense count: 9
69
109
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
70
110
  Metrics/MethodLength:
71
- Max: 52
111
+ Max: 72
112
+
113
+ # Offense count: 3
114
+ # Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
115
+ Metrics/ParameterLists:
116
+ Max: 6
72
117
 
73
118
  # Offense count: 1
74
119
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
75
120
  Metrics/PerceivedComplexity:
76
121
  Exclude:
77
- - 'lib/suma/schema_collection.rb'
122
+ - 'lib/suma/cli/links.rb'
123
+
124
+ # Offense count: 1
125
+ # Configuration parameters: MinSize.
126
+ Performance/CollectionLiteralInLoop:
127
+ Exclude:
128
+ - 'spec/suma/cli_spec.rb'
129
+
130
+ # Offense count: 1
131
+ # This cop supports safe autocorrection (--autocorrect).
132
+ # Configuration parameters: EnforcedStyle.
133
+ # SupportedStyles: implicit, each, example
134
+ RSpec/HookArgument:
135
+ Exclude:
136
+ - 'spec/suma/cli_spec.rb'
137
+
138
+ # Offense count: 1
139
+ RSpec/MultipleExpectations:
140
+ Max: 2
141
+
142
+ # Offense count: 1
143
+ # This cop supports safe autocorrection (--autocorrect).
144
+ # Configuration parameters: AutoCorrect, EnforcedStyle, AllowComments.
145
+ # SupportedStyles: empty, nil, both
146
+ Style/EmptyElse:
147
+ Exclude:
148
+ - 'lib/suma/cli/links.rb'
data/Gemfile CHANGED
@@ -5,8 +5,9 @@ source "https://rubygems.org"
5
5
  # Specify your gem's dependencies in suma.gemspec
6
6
  gemspec
7
7
 
8
- gem "rake", "~> 13.0"
9
- gem "rspec", "~> 3.0"
8
+ gem "rake"
9
+ gem "rspec"
10
10
  gem "rubocop"
11
11
  gem "rubocop-performance"
12
- gem "rubocop-rails"
12
+ gem "rubocop-rake"
13
+ gem "rubocop-rspec"
data/README.adoc CHANGED
@@ -12,15 +12,23 @@ to build the following artifacts:
12
12
 
13
13
  * the STEP Resource Library (SRL)
14
14
 
15
+ == Features
15
16
 
16
- == Install
17
+ * EXPRESS schema management for STEP standards
18
+ * Document collection building and compilation
19
+ * EXPRESS links validation and extraction
20
+ * Schema listing file generation
21
+ * Integration with the Metanorma ecosystem
22
+ * Progress tracking for schema loading operations
23
+
24
+ == Installation
17
25
 
18
26
  [source,sh]
19
27
  ----
20
28
  $ gem install suma
21
29
  ----
22
30
 
23
- == Usage
31
+ == Usage: CLI
24
32
 
25
33
  === General
26
34
 
@@ -30,17 +38,34 @@ $ gem install suma
30
38
  $ suma
31
39
  Commands:
32
40
  suma build METANORMA_SITE_MANIFEST # Build collection specified in site manifest (`metanorma*.yml`)
41
+ suma links SUBCOMMAND ...ARGS # Manage EXPRESS links
33
42
  suma help [COMMAND] # Describe available commands or one specific command
34
43
  ----
35
44
 
36
- Where:
45
+ === Build command
46
+
47
+ The `build` command processes a Metanorma site manifest and generates the specified output.
48
+
49
+ [source,sh]
50
+ ----
51
+ $ suma build METANORMA_SITE_MANIFEST [options]
52
+ ----
53
+
54
+ Parameters:
37
55
 
38
56
  `METANORMA_SITE_MANIFEST`:: This is the path to the Metanorma site manifest,
39
57
  typically `metanorma.yml`.
40
58
 
59
+ Options:
41
60
 
42
- === Compilation
61
+ `--[no-]compile`:: Compile or skip compilation of collection (default: true)
62
+ `--schemas-all-path=PATH`, `-s PATH`:: Generate file that contains all schemas in the collection
43
63
 
64
+ The generated `schemas-*.yaml` file name is derived from the input file name
65
+ with the word `metanorma` replaced with `schemas`.
66
+
67
+ [example]
68
+ ====
44
69
  .To compile SRL subset test collection
45
70
  [source,sh]
46
71
  ----
@@ -53,31 +78,108 @@ $ bundle exec suma build metanorma-test.yml
53
78
  $ bundle exec suma build metanorma-srl.yml
54
79
  ----
55
80
 
56
- // .To compile all STEPmod migrated docs (without detached) individually
57
- // [source,sh]
58
- // ----
59
- // $ bundle exec metanorma site generate
60
- // ----
81
+ .To generate schema listing without compilation
82
+ [source,sh]
83
+ ----
84
+ $ bundle exec suma build --no-compile metanorma-srl.yml
85
+ # => generates schemas-srl.yml
86
+ ----
87
+ ====
61
88
 
89
+ All documents need to have a `schemas.yaml` in their document root that lists
90
+ out which schemas the document includes.
62
91
 
63
- === Generating full schema listing files
92
+ === Links command
64
93
 
65
- We generate the full schema collection listing using the `--no-compile`
66
- option in the `suma` command.
94
+ ==== General
67
95
 
68
- The generated `schemas-*.yaml` file name is derived from the input file name
69
- with the word `metanorma` replaced with `schemas`.
96
+ The `links` command provides utilities for managing EXPRESS links.
97
+
98
+ ==== Extract and validate
99
+
100
+ Extracts and validates EXPRESS links without creating intermediate files.
70
101
 
71
102
  [source,sh]
72
103
  ----
73
- $ bundle exec suma build --no-compile metanorma-srl.yml
74
- # => generates schemas-srl.yml
104
+ $ suma links extract_and_validate SCHEMAS_FILE DOCUMENTS_PATH [OUTPUT_FILE]
75
105
  ----
76
106
 
77
- All documents need to have a `schemas.yaml` in their document root that lists
78
- out which schemas the document includes.
107
+ Parameters:
108
+
109
+ `SCHEMAS_FILE`:: Path to the schemas file (default: "schemas-srl.yml")
110
+
111
+ `DOCUMENTS_PATH`:: Path to the documents directory (default: "documents")
112
+
113
+ `OUTPUT_FILE`:: Path to write validation results (default: "validation_results.txt")
114
+
115
+ [example]
116
+ ====
117
+ .To validate EXPRESS links in documents
118
+ [source,sh]
119
+ ----
120
+ $ bundle exec suma links extract_and_validate schemas-srl.yml documents validation_results.txt
121
+ ----
122
+ ====
123
+
124
+ This command:
125
+
126
+ * Loads the schemas specified in the `SCHEMAS_FILE`
127
+ * Searches for EXPRESS links in all AsciiDoc files in the `DOCUMENTS_PATH`
128
+ * Validates these links against the loaded schemas
129
+ * Writes validation results to the `OUTPUT_FILE`
130
+ * Provides progress bars to track schema loading and link validation
131
+
132
+
133
+ == Usage: Ruby
134
+
135
+ === General
136
+
137
+ Suma can be used programmatically in your Ruby applications. The following examples demonstrate common usage patterns.
138
+
139
+ === Building collections
140
+
141
+ [source,ruby]
142
+ ----
143
+ require 'suma'
144
+
145
+ # Build a collection with default settings
146
+ Suma::Processor.run(
147
+ metanorma_yaml_path: "metanorma-srl.yml",
148
+ schemas_all_path: "schemas-srl.yml",
149
+ compile: true,
150
+ output_directory: "_site"
151
+ )
152
+
153
+ # Generate schema listing without compilation
154
+ Suma::Processor.run(
155
+ metanorma_yaml_path: "metanorma-srl.yml",
156
+ schemas_all_path: "schemas-srl.yml",
157
+ compile: false,
158
+ output_directory: "_site"
159
+ )
160
+ ----
161
+
162
+ === Working with schema configurations
163
+
164
+ [source,ruby]
165
+ ----
166
+ require 'suma'
167
+
168
+ # Load schemas using SchemaConfig
169
+ schemas_file_path = "schemas-srl.yml"
170
+ schemas_config = Suma::SchemaConfig::Config.from_yaml(IO.read(schemas_file_path))
171
+
172
+ # Set the initial path to resolve relative paths
173
+ schemas_config.set_initial_path(schemas_file_path)
174
+
175
+ # Access schema information
176
+ schemas_config.schemas.each do |schema|
177
+ puts "Schema ID: #{schema.id}"
178
+ puts "Schema path: #{schema.path}"
179
+ end
180
+ ----
79
181
 
80
182
 
81
- == License
183
+ == Copyright and license
82
184
 
83
185
  Copyright Ribose. BSD 2-clause license.
data/exe/suma CHANGED
@@ -3,4 +3,4 @@
3
3
 
4
4
  require "suma/cli"
5
5
 
6
- Suma::Cli.start(ARGV)
6
+ Suma::Cli::Core.start(ARGV)
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require_relative "../thor_ext"
5
+
6
+ module Suma
7
+ module Cli
8
+ # Build command for building collections
9
+ class Build < Thor
10
+ # Return true to exit with non-zero status on errors
11
+ def self.exit_on_failure?
12
+ true
13
+ end
14
+
15
+ default_command :default_build
16
+
17
+ desc "default_build METANORMA_SITE_MANIFEST",
18
+ "Build collection specified in site manifest (`metanorma*.yml`)"
19
+ option :compile, type: :boolean, default: true,
20
+ desc: "Compile or skip compile of collection"
21
+ option :schemas_all_path, type: :string, aliases: "-s",
22
+ desc: "Generate file that contains all schemas in the collection."
23
+
24
+ def default_build(metanorma_site_manifest)
25
+ # Lazy-load dependencies only when this command is actually used
26
+ require_relative "../processor"
27
+ require_relative "../utils"
28
+
29
+ unless File.exist?(metanorma_site_manifest)
30
+ raise Errno::ENOENT, "Specified Metanorma site manifest file " \
31
+ "`#{metanorma_site_manifest}` not found."
32
+ end
33
+
34
+ # Allow errors to propagate
35
+ run(metanorma_site_manifest, options)
36
+ end
37
+
38
+ private
39
+
40
+ def run(manifest, options)
41
+ # Set schemas_all_path to match metanorma_yaml_path
42
+ schemas_all_path = options[:schemas_all_path] ||
43
+ manifest.gsub("metanorma", "schemas")
44
+
45
+ Processor.run(
46
+ metanorma_yaml_path: manifest,
47
+ schemas_all_path: schemas_all_path,
48
+ compile: options[:compile],
49
+ output_directory: "_site",
50
+ )
51
+ end
52
+
53
+ def log_error(error)
54
+ Utils.log "[ERROR] Error occurred during processing. See details below."
55
+ Utils.log error
56
+ Utils.log error.inspect
57
+ Utils.log error.backtrace.join("\n")
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,446 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require_relative "../utils"
5
+
6
+ module Suma
7
+ module Cli
8
+ # Links command for managing EXPRESS links
9
+ class Links < Thor
10
+ desc "extract_and_validate SCHEMAS_FILE DOCUMENTS_PATH [OUTPUT_FILE]",
11
+ "Extract and validate express links without creating intermediate file"
12
+ def extract_and_validate(schemas_file = "schemas-srl.yml",
13
+ documents_path = "documents",
14
+ output_file = "validation_results.txt")
15
+ load_dependencies
16
+ paths = prepare_file_paths(schemas_file, documents_path, output_file)
17
+
18
+ # Load schemas and extract links
19
+ schemas_config = load_schemas_config(paths[:schemas_file])
20
+ exp_files = collect_schema_paths(schemas_config,
21
+ paths[:schemas_file_rel])
22
+ adoc_files = find_adoc_files(paths[:documents_path])
23
+
24
+ all_files = adoc_files + exp_files
25
+ display_file_counts(adoc_files, exp_files)
26
+
27
+ # Extract links from files
28
+ links_by_file = extract_links(all_files)
29
+
30
+ # Validate links against schemas
31
+ repo = load_express_schemas(schemas_config)
32
+ unresolved_links = validate_links(links_by_file, repo)
33
+
34
+ # Generate and output results
35
+ write_validation_results(paths[:output_file], paths[:output_file_rel],
36
+ unresolved_links, links_by_file)
37
+ end
38
+
39
+ private
40
+
41
+ # Load all required dependencies for link validation
42
+ def load_dependencies
43
+ # Lazy-load dependencies only when this command is actually used
44
+ require "expressir"
45
+ require "ruby-progressbar"
46
+ require_relative "../schema_config"
47
+ require "pathname"
48
+ end
49
+
50
+ # Prepare and normalize all file paths needed for the operation
51
+ def prepare_file_paths(schemas_file, documents_path, output_file)
52
+ # Convert to absolute paths
53
+ schemas_file_path = Pathname.new(schemas_file).expand_path
54
+ documents_path = Pathname.new(documents_path).expand_path
55
+ output_file_path = Pathname.new(output_file).expand_path
56
+
57
+ # Store relative paths for display
58
+ schemas_file_rel = Pathname.new(schemas_file_path).relative_path_from(Pathname.pwd).to_s
59
+ documents_path_rel = Pathname.new(documents_path).relative_path_from(Pathname.pwd).to_s
60
+ output_file_rel = Pathname.new(output_file_path).relative_path_from(Pathname.pwd).to_s
61
+
62
+ puts "Extracting and validating express links using schemas from #{schemas_file_rel}..."
63
+ puts "Looking for documents in #{documents_path_rel}..."
64
+
65
+ {
66
+ schemas_file: schemas_file_path,
67
+ schemas_file_rel: schemas_file_rel,
68
+ documents_path: documents_path,
69
+ documents_path_rel: documents_path_rel,
70
+ output_file: output_file_path,
71
+ output_file_rel: output_file_rel,
72
+ }
73
+ end
74
+
75
+ # Load and initialize the schemas configuration
76
+ def load_schemas_config(schemas_file_path)
77
+ schemas_config = Suma::SchemaConfig::Config.from_yaml(File.read(schemas_file_path))
78
+ # Ensure the config is initialized with the correct path to resolve relative paths
79
+ schemas_config.set_initial_path(schemas_file_path.to_s)
80
+ schemas_config
81
+ rescue StandardError => e
82
+ puts "Error loading schemas file: #{e.message}"
83
+ exit(1)
84
+ end
85
+
86
+ # Collect paths to all schema files from the config
87
+ def collect_schema_paths(schemas_config, schemas_file_rel)
88
+ exp_files = []
89
+ schemas_config.schemas.each do |schema|
90
+ exp_files << schema.path if schema.path
91
+ end
92
+
93
+ puts "Found #{exp_files.size} EXPRESS schema files from #{schemas_file_rel}"
94
+ exp_files
95
+ end
96
+
97
+ # Find all AsciiDoc files in the specified path
98
+ def find_adoc_files(documents_path)
99
+ Dir.glob(documents_path.join("**", "*.adoc").to_s)
100
+ end
101
+
102
+ # Display counts of discovered files
103
+ def display_file_counts(adoc_files, exp_files)
104
+ puts "Found #{adoc_files.size} AsciiDoc files and #{exp_files.size} EXPRESS files"
105
+ end
106
+
107
+ # Create a standardized progress bar
108
+ def create_progress_bar(title, total)
109
+ ProgressBar.create(
110
+ title: title,
111
+ total: total,
112
+ format: "%t: [%B] %p%% %c/%C %e",
113
+ progress_mark: "=",
114
+ remainder_mark: " ",
115
+ length: 80,
116
+ )
117
+ end
118
+
119
+ # Extract EXPRESS links from all files
120
+ def extract_links(files)
121
+ links_by_file = {}
122
+ link_count = 0
123
+
124
+ progress = create_progress_bar("Processing files", files.size)
125
+
126
+ files.each do |file|
127
+ progress.increment
128
+ begin
129
+ content = File.read(file)
130
+ # Extract links while ignoring text after comma
131
+ express_links = content.scan(/<<express:([^,>]+)(?:,[^>]+)?>>/).flatten.uniq
132
+
133
+ if express_links.any?
134
+ links_by_file[file] = express_links
135
+ link_count += express_links.size
136
+ end
137
+ rescue StandardError => e
138
+ puts "\nWarning: Could not read file #{file}: #{e.message}"
139
+ end
140
+ end
141
+
142
+ puts "\nExtracted #{link_count} unique express links from #{links_by_file.size} files"
143
+ links_by_file
144
+ end
145
+
146
+ # Load all EXPRESS schemas for validation
147
+ def load_express_schemas(schemas_config)
148
+ # Get all schema paths for validation
149
+ schema_paths = {}
150
+ schemas_config.schemas.each do |schema|
151
+ schema_paths[schema.id] = schema.path
152
+ end
153
+
154
+ puts "Loading #{schema_paths.size} EXPRESS schemas for validation..."
155
+
156
+ # Setup progress bar for schema loading
157
+ loading_progress = create_progress_bar("Loading schemas",
158
+ schema_paths.size)
159
+
160
+ # Try to load all schemas with progress tracking
161
+ begin
162
+ repo = Expressir::Express::Parser.from_files(schema_paths.values) do |filename, _schemas, error|
163
+ loading_progress.increment
164
+ if error
165
+ puts "\nWarning: Error loading schema #{filename}: #{error.message}"
166
+ end
167
+ end
168
+
169
+ puts "Successfully loaded #{repo.schemas.size} schemas"
170
+ repo
171
+ rescue StandardError => e
172
+ puts "Error loading schemas: #{e.message}"
173
+ exit(1)
174
+ end
175
+ end
176
+
177
+ # Validate all links against loaded schemas
178
+ def validate_links(links_by_file, repo)
179
+ unresolved_links = []
180
+ total_links = links_by_file.values.sum(&:size)
181
+
182
+ progress = create_progress_bar("Validating links", total_links)
183
+
184
+ links_by_file.each do |file, links|
185
+ validate_file_links(file, links, repo, progress, unresolved_links)
186
+ end
187
+
188
+ unresolved_links
189
+ end
190
+
191
+ # Validate links in a specific file
192
+ def validate_file_links(file, links, repo, progress, unresolved_links)
193
+ file_content = File.read(file)
194
+ file_lines = file_content.lines
195
+
196
+ links.each do |link|
197
+ progress.increment
198
+ line_idx = find_link_line(file_lines, link)
199
+ next unless line_idx
200
+
201
+ # Parse link (schema only, schema.element, or schema.element.path)
202
+ parts = link.split(".")
203
+
204
+ if parts.size == 1
205
+ validate_schema_only_link(parts[0], repo, file, line_idx, link,
206
+ unresolved_links)
207
+ else
208
+ validate_schema_element_link(parts, repo, file, line_idx, link,
209
+ unresolved_links)
210
+ end
211
+ end
212
+ rescue StandardError => e
213
+ puts "Warning: Error processing file #{file}: #{e.message}"
214
+ end
215
+
216
+ # Find the line where a link appears in the file
217
+ def find_link_line(file_lines, link)
218
+ file_lines.each_with_index do |line, idx|
219
+ # Match both with and without comma text
220
+ if /<<express:#{Regexp.escape(link)}(?:,[^>]+)?>>/.match?(line)
221
+ return idx
222
+ end
223
+ end
224
+ nil
225
+ end
226
+
227
+ # Validate a link that only references a schema
228
+ def validate_schema_only_link(schema_name, repo, file, line_idx, link,
229
+ unresolved_links)
230
+ # Check if schema exists
231
+ schema = repo.schemas.find do |s|
232
+ s.id.downcase == schema_name.downcase
233
+ end
234
+
235
+ if !schema
236
+ unresolved_links << {
237
+ file: file,
238
+ line: line_idx + 1,
239
+ link: link,
240
+ reason: "Schema '#{schema_name}' not found",
241
+ }
242
+ end
243
+ end
244
+
245
+ # Validate a link with schema.element or deeper paths
246
+ def validate_schema_element_link(parts, repo, file, line_idx, link,
247
+ unresolved_links)
248
+ schema_name = parts[0]
249
+ element_name = parts[1]
250
+
251
+ # Check if schema exists
252
+ schema = repo.schemas.find do |s|
253
+ s.id.downcase == schema_name.downcase
254
+ end
255
+
256
+ if !schema
257
+ unresolved_links << {
258
+ file: file,
259
+ line: line_idx + 1,
260
+ link: link,
261
+ reason: "Schema '#{schema_name}' not found",
262
+ }
263
+ return
264
+ end
265
+
266
+ # Find the element in the schema
267
+ element = find_schema_element(schema, element_name)
268
+
269
+ if !element
270
+ unresolved_links << {
271
+ file: file,
272
+ line: line_idx + 1,
273
+ link: link,
274
+ reason: "Element '#{element_name}' not found in schema '#{schema_name}'",
275
+ }
276
+ return
277
+ end
278
+
279
+ # If we have more than 2 parts, validate the deeper path
280
+ if parts.size > 2
281
+ validation_error = validate_deep_path(schema, element, parts[2..],
282
+ file, line_idx, link)
283
+ unresolved_links << validation_error if validation_error
284
+ end
285
+ end
286
+
287
+ # Find an element in a schema by name
288
+ def find_schema_element(schema, element_name)
289
+ # Try to find element in various collections
290
+ element_collections = [
291
+ schema.entities,
292
+ schema.types,
293
+ schema.constants,
294
+ schema.functions,
295
+ schema.rules,
296
+ schema.procedures,
297
+ schema.subtype_constraints,
298
+ ]
299
+
300
+ element_collections.each do |collection|
301
+ element = collection&.find do |e|
302
+ e.id.downcase == element_name.downcase
303
+ end
304
+ return element if element
305
+ end
306
+
307
+ nil
308
+ end
309
+
310
+ # Validate deeper paths in a link (schema.element.path.subpath)
311
+ def validate_deep_path(schema, element, path_parts, file, line_idx,
312
+ full_link)
313
+ current_element = element
314
+ current_path = "#{schema.id}.#{element.id}"
315
+
316
+ # Process each part of the path
317
+ path_parts.each do |part|
318
+ # The validation logic depends on the type of current element
319
+ case current_element
320
+ when Expressir::Express::Entity
321
+ # For entities, check attributes
322
+ attribute = current_element.attributes&.find do |a|
323
+ a.id.downcase == part.downcase
324
+ end
325
+
326
+ unless attribute
327
+ return {
328
+ file: file,
329
+ line: line_idx + 1,
330
+ link: full_link,
331
+ reason: "Attribute '#{part}' not found in entity '#{current_path}'",
332
+ }
333
+ end
334
+
335
+ current_element = attribute
336
+ current_path += ".#{part}"
337
+
338
+ when Expressir::Express::Type
339
+ # For types, validation depends on type kind
340
+ if current_element.respond_to?(:base_type) && current_element.base_type
341
+ # For derived types, find the base type
342
+ base_type = find_base_type(schema, current_element.base_type)
343
+
344
+ unless base_type
345
+ return {
346
+ file: file,
347
+ line: line_idx + 1,
348
+ link: full_link,
349
+ reason: "Base type not found for '#{current_path}'",
350
+ }
351
+ end
352
+
353
+ # Continue validation using base type
354
+ current_element = base_type
355
+
356
+ elsif current_element.respond_to?(:elements) && current_element.elements
357
+ # For enum types, check enum values
358
+ enum_value = current_element.elements.find do |e|
359
+ e.id.downcase == part.downcase
360
+ end
361
+
362
+ unless enum_value
363
+ return {
364
+ file: file,
365
+ line: line_idx + 1,
366
+ link: full_link,
367
+ reason: "Enumeration value '#{part}' not found in type '#{current_path}'",
368
+ }
369
+ end
370
+
371
+ current_element = enum_value
372
+ current_path += ".#{part}"
373
+
374
+ else
375
+ # For other types, we can't navigate deeper
376
+ return {
377
+ file: file,
378
+ line: line_idx + 1,
379
+ link: full_link,
380
+ reason: "Cannot navigate deeper from type '#{current_path}'",
381
+ }
382
+ end
383
+
384
+ else
385
+ # For other element types, navigation is not supported
386
+ return {
387
+ file: file,
388
+ line: line_idx + 1,
389
+ link: full_link,
390
+ reason: "Cannot navigate deeper from '#{current_path}'",
391
+ }
392
+ end
393
+ end
394
+
395
+ # If we've processed all parts without returning an error, path is valid
396
+ nil
397
+ end
398
+
399
+ # Find a base type in a schema
400
+ def find_base_type(schema, type_ref)
401
+ # Skip built-in types
402
+ return nil if %w[INTEGER REAL STRING BOOLEAN NUMBER BINARY
403
+ LOGICAL].include?(type_ref.to_s.upcase)
404
+
405
+ # Find the referenced type in the schema
406
+ if type_ref.is_a?(String)
407
+ find_schema_element(schema, type_ref)
408
+ elsif type_ref.respond_to?(:id)
409
+ # It's already a type object
410
+ type_ref
411
+ else
412
+ nil
413
+ end
414
+ end
415
+
416
+ # Write validation results to the output file
417
+ def write_validation_results(output_file_path, output_file_rel,
418
+ unresolved_links, links_by_file)
419
+ total_links = links_by_file.values.sum(&:size)
420
+
421
+ # Prepare results for output
422
+ results = []
423
+ results << "Validation complete. Checked #{total_links} links."
424
+
425
+ if unresolved_links.empty?
426
+ results << "✅ All links resolved successfully!"
427
+ else
428
+ results << "❌ Found #{unresolved_links.size} unresolved links:"
429
+ unresolved_links.each do |issue|
430
+ results << "#{issue[:file]}:#{issue[:line]} - <<express:#{issue[:link]}>> - #{issue[:reason]}"
431
+ end
432
+ end
433
+
434
+ # Write results to output file
435
+ begin
436
+ File.write(output_file_path, results.join("\n"))
437
+ puts "Validation results written to #{output_file_rel}"
438
+ rescue StandardError => e
439
+ puts "Error writing to output file: #{e.message}"
440
+ # Still print results to console as fallback
441
+ puts results
442
+ end
443
+ end
444
+ end
445
+ end
446
+ end
data/lib/suma/cli.rb CHANGED
@@ -2,53 +2,27 @@
2
2
 
3
3
  require "thor"
4
4
  require_relative "thor_ext"
5
- require_relative "processor"
6
- require_relative "utils"
7
5
 
8
6
  module Suma
9
- class Cli < Thor
10
- extend ThorExt::Start
11
-
12
- desc "build METANORMA_SITE_MANIFEST",
13
- "Build collection specified in site manifest (`metanorma*.yml`)"
14
- option :compile, type: :boolean, default: true,
15
- desc: "Compile or skip compile of collection"
16
- option :schemas_all_path, type: :string, aliases: "-s",
17
- desc: "Generate file that contains all schemas in the collection."
18
-
19
- def build(metanorma_site_manifest)
20
- unless File.exist?(metanorma_site_manifest)
21
- raise Errno::ENOENT, "Specified Metanorma site manifest file " \
22
- "`#{metanorma_site_manifest}` not found."
7
+ module Cli
8
+ # Core command class for handling CLI entrypoints
9
+ class Core < Thor
10
+ extend ThorExt::Start
11
+
12
+ desc "build METANORMA_SITE_MANIFEST",
13
+ "Build collection specified in site manifest (`metanorma*.yml`)"
14
+ def build(*args)
15
+ # If no arguments, add an empty array to ensure the default command is triggered
16
+ args = [] if args.empty?
17
+ require_relative "cli/build"
18
+ Cli::Build.start(args)
23
19
  end
24
20
 
25
- begin
26
- run(metanorma_site_manifest, options)
27
- rescue StandardError => e
28
- log_error(e)
21
+ desc "links SUBCOMMAND ...ARGS", "Manage EXPRESS links"
22
+ def links(*args)
23
+ require_relative "cli/links"
24
+ Cli::Links.start(args)
29
25
  end
30
26
  end
31
-
32
- private
33
-
34
- def run(manifest, options)
35
- # Set schemas_all_path to match metanorma_yaml_path
36
- schemas_all_path = options[:schemas_all_path] ||
37
- manifest.gsub("metanorma", "schemas")
38
-
39
- Processor.run(
40
- metanorma_yaml_path: manifest,
41
- schemas_all_path: schemas_all_path,
42
- compile: options[:compile],
43
- output_directory: "_site",
44
- )
45
- end
46
-
47
- def log_error(error)
48
- Utils.log "[ERROR] Error occurred during processing. See details below."
49
- Utils.log error
50
- Utils.log error.inspect
51
- Utils.log error.backtrace.join("\n")
52
- end
53
27
  end
54
28
  end
@@ -16,7 +16,7 @@ module Suma
16
16
  end
17
17
 
18
18
  def to_file(path)
19
- File.open(path, "w") { |f| f.write to_yaml }
19
+ File.write(path, to_yaml)
20
20
  end
21
21
  end
22
22
  end
@@ -49,9 +49,7 @@ module Suma
49
49
  # return if File.exist?(filename_plain)
50
50
  FileUtils.mkdir_p(File.dirname(filename_plain))
51
51
 
52
- File.open(filename_plain, "w") do |file|
53
- file.write(to_plain)
54
- end
52
+ File.write(filename_plain, to_plain)
55
53
  end
56
54
  end
57
55
  end
@@ -12,9 +12,12 @@ require "metanorma/collection/collection"
12
12
  module Suma
13
13
  class Processor
14
14
  class << self
15
- def run(metanorma_yaml_path:, schemas_all_path:, compile:, output_directory: "_site")
15
+ # rubocop:disable Metrics/MethodLength
16
+ def run(metanorma_yaml_path:, schemas_all_path:, compile:,
17
+ output_directory: "_site")
16
18
  Utils.log "Current directory: #{Dir.getwd}, writing #{schemas_all_path}..."
17
- collection_config = export_schema_config(metanorma_yaml_path, schemas_all_path)
19
+ collection_config = export_schema_config(metanorma_yaml_path,
20
+ schemas_all_path)
18
21
 
19
22
  unless compile
20
23
  Utils.log "No compile option set. Skipping schema compilation."
@@ -27,6 +30,7 @@ module Suma
27
30
  Utils.log "Compiling complete collection..."
28
31
  compile_collection(collection_config, output_directory)
29
32
  end
33
+ # rubocop:enable Metrics/MethodLength
30
34
 
31
35
  private
32
36
 
@@ -61,7 +65,9 @@ module Suma
61
65
  end
62
66
 
63
67
  def compile_collection(collection_config, output_directory)
64
- metanorma_collection, collection_opts = build_collection(collection_config, output_directory)
68
+ metanorma_collection, collection_opts = build_collection(
69
+ collection_config, output_directory
70
+ )
65
71
 
66
72
  metanorma_collection.render(collection_opts)
67
73
 
@@ -59,9 +59,7 @@ module Suma
59
59
 
60
60
  # Utils.log "relative_path #{relative_path}"
61
61
 
62
- File.open(filename_adoc, "w") do |file|
63
- file.write(to_adoc(relative_path))
64
- end
62
+ File.write(filename_adoc, to_adoc(relative_path))
65
63
  end
66
64
 
67
65
  def filename_config
@@ -102,7 +100,8 @@ module Suma
102
100
 
103
101
  relative_path = Pathname.new(filename_adoc).relative_path_from(Dir.pwd)
104
102
  Utils.log "Compiling schema (id: #{id}, type: #{self.class}) => #{relative_path}"
105
- Metanorma::Compile.new.compile(filename_adoc, agree_to_terms: true, install_fonts: false)
103
+ Metanorma::Compile.new.compile(filename_adoc, agree_to_terms: true,
104
+ install_fonts: false)
106
105
  Utils.log "Compiling schema (id: #{id}, type: #{self.class}) => #{relative_path}... done!"
107
106
 
108
107
  # clean_artifacts
@@ -13,7 +13,7 @@ module Suma
13
13
 
14
14
  def initialize(**args)
15
15
  @path = path_relative_to_absolute(path) if path
16
- super(**args)
16
+ super
17
17
  end
18
18
 
19
19
  def base_path
@@ -31,9 +31,7 @@ module Suma
31
31
  end
32
32
 
33
33
  def to_file(to_path = path)
34
- File.open(to_path, "w") do |f|
35
- f.write(to_yaml)
36
- end
34
+ File.write(to_path, to_yaml)
37
35
  end
38
36
 
39
37
  def set_initial_path(new_path)
@@ -5,7 +5,7 @@ require_relative "schema_attachment"
5
5
  module Suma
6
6
  class SchemaDocument < SchemaAttachment
7
7
  def bookmark(anchor)
8
- a = anchor.gsub(/\}\}/, ' | replace: "\", "-"}}')
8
+ a = anchor.gsub("}}", ' | replace: "\", "-"}}')
9
9
  "[[#{@id}.#{a}]]"
10
10
  end
11
11
 
data/lib/suma/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Suma
4
- VERSION = "0.1.8"
4
+ VERSION = "0.1.9"
5
5
  end
data/suma.gemspec CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
9
9
  spec.email = ["open.source@ribose.com"]
10
10
 
11
11
  spec.summary = "Utility for SUMA " \
12
- "(STEP Unified Model-Based Standards Architecture)"
12
+ "(STEP Unified Model-Based Standards Architecture)"
13
13
  spec.description = <<~DESCRIPTION
14
14
  Utility for SUMA (STEP Unified Model-Based Standards Architecture)
15
15
  DESCRIPTION
@@ -20,7 +20,6 @@ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
20
20
  spec.bindir = "bin"
21
21
  spec.require_paths = ["lib"]
22
22
  spec.files = `git ls-files`.split("\n")
23
- spec.test_files = `git ls-files -- {spec}/*`.split("\n")
24
23
  spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
25
24
 
26
25
  # Specify which files should be added to the gem when it is released.
@@ -34,8 +33,10 @@ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
34
33
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
35
34
  spec.require_paths = ["lib"]
36
35
 
37
- spec.add_dependency "expressir"
36
+ spec.add_dependency "expressir", "~> 2.1.16"
38
37
  spec.add_dependency "lutaml-model", "~> 0.7"
39
38
  spec.add_dependency "metanorma-cli"
39
+ spec.add_dependency "ruby-progressbar"
40
40
  spec.add_dependency "thor", ">= 0.20"
41
+ spec.metadata["rubygems_mfa_required"] = "true"
41
42
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: suma
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -14,16 +14,16 @@ dependencies:
14
14
  name: expressir
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: 2.1.16
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: 2.1.16
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: lutaml-model
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: ruby-progressbar
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: thor
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -91,6 +105,8 @@ files:
91
105
  - exe/suma
92
106
  - lib/suma.rb
93
107
  - lib/suma/cli.rb
108
+ - lib/suma/cli/build.rb
109
+ - lib/suma/cli/links.rb
94
110
  - lib/suma/collection_config.rb
95
111
  - lib/suma/collection_manifest.rb
96
112
  - lib/suma/express_schema.rb
@@ -110,7 +126,8 @@ files:
110
126
  homepage: https://github.com/metanorma/suma
111
127
  licenses:
112
128
  - BSD-2-Clause
113
- metadata: {}
129
+ metadata:
130
+ rubygems_mfa_required: 'true'
114
131
  post_install_message:
115
132
  rdoc_options: []
116
133
  require_paths: