suma 0.2.5 → 0.3.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/.github/workflows/rake.yml +3 -0
- data/.github/workflows/release.yml +5 -1
- data/.gitignore +10 -1
- data/.rubocop_todo.yml +237 -28
- data/CLAUDE.md +102 -0
- data/Gemfile +3 -1
- data/README.adoc +188 -1
- data/exe/suma +1 -1
- data/lib/suma/cli/build.rb +2 -8
- data/lib/suma/cli/check_svg_quality.rb +172 -0
- data/lib/suma/cli/compare.rb +6 -158
- data/lib/suma/cli/convert_jsdai.rb +0 -2
- data/lib/suma/cli/core.rb +119 -0
- data/lib/suma/cli/export.rb +1 -10
- data/lib/suma/cli/extract_terms.rb +10 -654
- data/lib/suma/cli/generate_register.rb +34 -0
- data/lib/suma/cli/generate_schemas.rb +8 -124
- data/lib/suma/cli/reformat.rb +0 -1
- data/lib/suma/cli/validate.rb +0 -2
- data/lib/suma/cli/validate_links.rb +14 -291
- data/lib/suma/cli.rb +12 -102
- data/lib/suma/collection_config.rb +0 -2
- data/lib/suma/collection_manifest.rb +7 -111
- data/lib/suma/eengine/wrapper.rb +0 -1
- data/lib/suma/eengine.rb +8 -0
- data/lib/suma/express_schema.rb +43 -31
- data/lib/suma/jsdai/figure.rb +0 -3
- data/lib/suma/jsdai/figure_xml.rb +12 -9
- data/lib/suma/jsdai.rb +5 -8
- data/lib/suma/link_validator.rb +211 -0
- data/lib/suma/manifest_traverser.rb +92 -0
- data/lib/suma/processor.rb +76 -105
- data/lib/suma/register_manifest_generator.rb +163 -0
- data/lib/suma/schema_category.rb +83 -0
- data/lib/suma/schema_collection.rb +28 -63
- data/lib/suma/schema_comparer.rb +117 -0
- data/lib/suma/schema_compiler.rb +86 -0
- data/lib/suma/schema_discovery.rb +75 -0
- data/lib/suma/schema_exporter.rb +7 -35
- data/lib/suma/schema_index.rb +53 -0
- data/lib/suma/schema_manifest_generator.rb +113 -0
- data/lib/suma/schema_naming.rb +111 -0
- data/lib/suma/schema_template/document.rb +141 -0
- data/lib/suma/schema_template/plain.rb +46 -0
- data/lib/suma/schema_template.rb +19 -0
- data/lib/suma/svg_quality/batch_report.rb +78 -0
- data/lib/suma/svg_quality/formatters/json_formatter.rb +30 -0
- data/lib/suma/svg_quality/formatters/terminal_formatter.rb +168 -0
- data/lib/suma/svg_quality/formatters/yaml_formatter.rb +32 -0
- data/lib/suma/svg_quality/formatters.rb +12 -0
- data/lib/suma/svg_quality/report.rb +52 -0
- data/lib/suma/svg_quality.rb +30 -0
- data/lib/suma/term_extractor.rb +466 -0
- data/lib/suma/urn.rb +61 -0
- data/lib/suma/utils.rb +10 -2
- data/lib/suma/version.rb +1 -1
- data/lib/suma.rb +34 -5
- data/suma.gemspec +3 -2
- metadata +53 -9
- data/lib/suma/export_standalone_schema.rb +0 -14
- data/lib/suma/schema_attachment.rb +0 -130
- data/lib/suma/schema_document.rb +0 -132
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
6
|
+
module Suma
|
|
7
|
+
class SchemaComparer
|
|
8
|
+
attr_reader :trial_schema, :reference_schema, :options
|
|
9
|
+
|
|
10
|
+
def initialize(trial_schema, reference_schema, options = {})
|
|
11
|
+
@trial_schema = trial_schema
|
|
12
|
+
@reference_schema = reference_schema
|
|
13
|
+
@options = options
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def compare
|
|
17
|
+
validate_inputs
|
|
18
|
+
|
|
19
|
+
trial_stepmod = options[:trial_stepmod] || detect_repo_root(trial_schema)
|
|
20
|
+
reference_stepmod = options[:reference_stepmod] || detect_repo_root(reference_schema)
|
|
21
|
+
|
|
22
|
+
out_dir = Dir.mktmpdir("eengine-compare-")
|
|
23
|
+
|
|
24
|
+
result = Eengine::Wrapper.compare(
|
|
25
|
+
trial_schema,
|
|
26
|
+
reference_schema,
|
|
27
|
+
mode: options[:mode] || "resource",
|
|
28
|
+
trial_stepmod: trial_stepmod,
|
|
29
|
+
reference_stepmod: reference_stepmod,
|
|
30
|
+
out_dir: out_dir,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
unless result[:has_changes]
|
|
34
|
+
FileUtils.rm_rf(out_dir) if File.directory?(out_dir)
|
|
35
|
+
return nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
unless result[:xml_path]
|
|
39
|
+
raise Suma::CompilationError,
|
|
40
|
+
"XML output not found"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
convert_to_change_yaml(result[:xml_path], out_dir)
|
|
44
|
+
ensure
|
|
45
|
+
FileUtils.rm_rf(out_dir) if out_dir && File.directory?(out_dir)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def validate_inputs
|
|
51
|
+
unless File.exist?(trial_schema)
|
|
52
|
+
raise Suma::SchemaNotFoundError,
|
|
53
|
+
"Trial schema not found: #{trial_schema}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
unless File.exist?(reference_schema)
|
|
57
|
+
raise Suma::SchemaNotFoundError,
|
|
58
|
+
"Reference schema not found: #{reference_schema}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
unless Eengine::Wrapper.available?
|
|
62
|
+
raise Suma::EengineNotAvailableError,
|
|
63
|
+
"eengine not found in PATH. Install from:\n " \
|
|
64
|
+
"macOS: https://github.com/expresslang/homebrew-eengine\n " \
|
|
65
|
+
"Linux: https://github.com/expresslang/eengine-releases"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def detect_repo_root(schema_path)
|
|
70
|
+
current = File.expand_path(File.dirname(schema_path))
|
|
71
|
+
|
|
72
|
+
loop do
|
|
73
|
+
return current if File.directory?(File.join(current, ".git"))
|
|
74
|
+
|
|
75
|
+
parent = File.dirname(current)
|
|
76
|
+
break if parent == current
|
|
77
|
+
|
|
78
|
+
current = parent
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
File.dirname(schema_path)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def convert_to_change_yaml(xml_path, _out_dir)
|
|
85
|
+
schema_name = extract_schema_name(trial_schema)
|
|
86
|
+
output_path = determine_output_path
|
|
87
|
+
|
|
88
|
+
existing_schema = nil
|
|
89
|
+
if File.exist?(output_path)
|
|
90
|
+
require "expressir/changes"
|
|
91
|
+
existing_schema = Expressir::Changes::SchemaChange.from_file(output_path)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
converter = EengineConverter.new(xml_path, schema_name)
|
|
95
|
+
change_schema = converter.convert(
|
|
96
|
+
version: options[:version],
|
|
97
|
+
existing_change_schema: existing_schema,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
change_schema.to_file(output_path)
|
|
101
|
+
output_path
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def extract_schema_name(path)
|
|
105
|
+
basename = File.basename(path, ".exp")
|
|
106
|
+
basename.sub(/_\d+$/, "")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def determine_output_path
|
|
110
|
+
options[:output] || begin
|
|
111
|
+
base = extract_schema_name(trial_schema)
|
|
112
|
+
dir = File.dirname(trial_schema)
|
|
113
|
+
File.join(dir, "#{base}.changes.yaml")
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
module Suma
|
|
7
|
+
# Orchestrates the compilation of a single EXPRESS schema into HTML/XML
|
|
8
|
+
# via Metanorma.
|
|
9
|
+
#
|
|
10
|
+
# SchemaCompiler owns all file I/O and the Metanorma::Compile invocation
|
|
11
|
+
# for one schema; the rendered AsciiDoc body is supplied by a SchemaTemplate
|
|
12
|
+
# that the caller injects. This split means templates can be tested as pure
|
|
13
|
+
# functions and the compiler can be tested with a real adoc fixture, without
|
|
14
|
+
# either depending on the other.
|
|
15
|
+
class SchemaCompiler
|
|
16
|
+
attr_reader :schema, :id, :output_path, :template
|
|
17
|
+
|
|
18
|
+
def initialize(schema:, output_path:, template:)
|
|
19
|
+
@schema = schema
|
|
20
|
+
@id = schema.id
|
|
21
|
+
@output_path = output_path
|
|
22
|
+
@template = template
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def compile
|
|
26
|
+
save_config
|
|
27
|
+
save_adoc
|
|
28
|
+
invoke_metanorma
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def output_xml_path
|
|
33
|
+
filename_adoc("xml")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def extensions
|
|
37
|
+
template.extensions
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def filename_adoc(ext = "adoc")
|
|
43
|
+
File.join(output_path, "doc_#{id}.#{ext}")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def filename_config
|
|
47
|
+
File.join(output_path, "schema_#{id}.yaml")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def save_adoc
|
|
51
|
+
log_relative "Save EXPRESS adoc", filename_adoc
|
|
52
|
+
|
|
53
|
+
FileUtils.mkdir_p(File.dirname(filename_adoc))
|
|
54
|
+
|
|
55
|
+
config_relative = Pathname.new(filename_config)
|
|
56
|
+
.relative_path_from(Pathname.new(File.dirname(filename_adoc)))
|
|
57
|
+
|
|
58
|
+
File.write(filename_adoc, template.render(config_relative))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def save_config
|
|
62
|
+
log_relative "Save schema config", filename_config
|
|
63
|
+
|
|
64
|
+
FileUtils.mkdir_p(File.dirname(filename_config))
|
|
65
|
+
config = Expressir::SchemaManifest.new
|
|
66
|
+
config.schemas << Expressir::SchemaManifestEntry.new(id: id,
|
|
67
|
+
path: schema.path)
|
|
68
|
+
config.save_to_path(filename_config)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def invoke_metanorma
|
|
72
|
+
log_relative "Compiling schema (id: #{id})", filename_adoc
|
|
73
|
+
Metanorma::Compile.new.compile(
|
|
74
|
+
filename_adoc,
|
|
75
|
+
agree_to_terms: true,
|
|
76
|
+
install_fonts: false,
|
|
77
|
+
)
|
|
78
|
+
log_relative "Compiling schema (id: #{id}) ... done!", filename_adoc
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def log_relative(prefix, path)
|
|
82
|
+
relative = Pathname.new(path).relative_path_from(Dir.pwd)
|
|
83
|
+
Utils.log "#{prefix}: #{relative}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Suma
|
|
4
|
+
# Service for schema-config I/O on a single CollectionManifest node.
|
|
5
|
+
#
|
|
6
|
+
# SchemaDiscovery owns two concerns that previously lived on
|
|
7
|
+
# CollectionManifest:
|
|
8
|
+
#
|
|
9
|
+
# 1. Loading the `schemas.yaml` that sits next to a `collection.yml`
|
|
10
|
+
# into the manifest's `schema_config` slot.
|
|
11
|
+
# 2. Building the doc CollectionManifest sub-tree that hosts the
|
|
12
|
+
# compiled XML output for each schema in `schema_config`.
|
|
13
|
+
#
|
|
14
|
+
# It does not walk the manifest tree — that is ManifestTraverser's job.
|
|
15
|
+
# The split keeps schema I/O in one place and tree traversal in another,
|
|
16
|
+
# so each is independently testable.
|
|
17
|
+
class SchemaDiscovery
|
|
18
|
+
attr_reader :manifest
|
|
19
|
+
|
|
20
|
+
def initialize(manifest)
|
|
21
|
+
@manifest = manifest
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# If the manifest's file is a `collection.yml` and a `schemas.yaml`
|
|
25
|
+
# sits alongside it, parse it into an Expressir::SchemaManifest and
|
|
26
|
+
# store it on the manifest. Otherwise leave `schema_config` untouched.
|
|
27
|
+
def load_config
|
|
28
|
+
return unless manifest.file
|
|
29
|
+
return unless File.basename(manifest.file) == "collection.yml"
|
|
30
|
+
|
|
31
|
+
schemas_yaml_path = File.join(File.dirname(manifest.file), "schemas.yaml")
|
|
32
|
+
return unless File.exist?(schemas_yaml_path)
|
|
33
|
+
|
|
34
|
+
manifest.schema_config = Expressir::SchemaManifest.from_file(schemas_yaml_path)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Build a CollectionManifest sub-tree that hosts the compiled XML for
|
|
38
|
+
# every schema in `manifest.schema_config`. The wrapper is named with
|
|
39
|
+
# a trailing underscore so it does not collide with the parent id.
|
|
40
|
+
def build_added_manifest(schema_output_path)
|
|
41
|
+
doc = CollectionConfig.from_file(manifest.file)
|
|
42
|
+
doc_id = doc.bibdata.id
|
|
43
|
+
|
|
44
|
+
added = CollectionManifest.new(
|
|
45
|
+
title: "Collection",
|
|
46
|
+
type: "collection",
|
|
47
|
+
identifier: "#{manifest.identifier}_",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
added.entry = [
|
|
51
|
+
CollectionManifest.new(
|
|
52
|
+
title: doc_id,
|
|
53
|
+
type: "document",
|
|
54
|
+
entry: build_doc_entries(schema_output_path),
|
|
55
|
+
),
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
added
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Build one CollectionManifest per schema in schema_config, naming
|
|
62
|
+
# each output file as `doc_<basename>.xml` under
|
|
63
|
+
# `<schema_output_path>/<schema_id>/`.
|
|
64
|
+
def build_doc_entries(schema_output_path)
|
|
65
|
+
manifest.schema_config.schemas.map do |schema|
|
|
66
|
+
xml_basename = "#{File.basename(schema.path, '.exp')}.xml"
|
|
67
|
+
CollectionManifest.new(
|
|
68
|
+
identifier: schema.id,
|
|
69
|
+
title: schema.id,
|
|
70
|
+
file: File.join(schema_output_path, schema.id, "doc_#{xml_basename}"),
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/lib/suma/schema_exporter.rb
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "express_schema"
|
|
4
|
-
require_relative "utils"
|
|
5
|
-
require_relative "export_standalone_schema"
|
|
6
3
|
require "fileutils"
|
|
7
4
|
|
|
8
5
|
module Suma
|
|
@@ -43,53 +40,28 @@ module Suma
|
|
|
43
40
|
end
|
|
44
41
|
|
|
45
42
|
def export_single_schema(schema)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
is_standalone_file = schema.is_a?(ExportStandaloneSchema)
|
|
49
|
-
schema_output_path = determine_output_path(schema, is_standalone_file)
|
|
43
|
+
is_standalone = !schema.is_a?(Expressir::SchemaManifestEntry)
|
|
44
|
+
schema_output_path = determine_output_path(schema, is_standalone)
|
|
50
45
|
|
|
51
46
|
express_schema = ExpressSchema.new(
|
|
52
47
|
id: schema.id,
|
|
53
48
|
path: schema.path.to_s,
|
|
54
49
|
output_path: schema_output_path,
|
|
55
|
-
is_standalone_file:
|
|
50
|
+
is_standalone_file: is_standalone,
|
|
56
51
|
)
|
|
57
52
|
|
|
58
53
|
express_schema.save_exp(with_annotations: options[:annotations])
|
|
59
54
|
end
|
|
60
55
|
|
|
61
|
-
def determine_output_path(schema,
|
|
62
|
-
if
|
|
63
|
-
# For standalone files, output directly to the root
|
|
56
|
+
def determine_output_path(schema, is_standalone)
|
|
57
|
+
if is_standalone
|
|
64
58
|
output_path.to_s
|
|
65
59
|
else
|
|
66
|
-
|
|
67
|
-
category
|
|
68
|
-
output_path.join(category).to_s
|
|
60
|
+
category = SchemaCategory.for_schema(id: schema.id, path: schema.path)
|
|
61
|
+
output_path.join(category.directory).to_s
|
|
69
62
|
end
|
|
70
63
|
end
|
|
71
64
|
|
|
72
|
-
# rubocop:disable Metrics/MethodLength
|
|
73
|
-
def categorize_schema(schema)
|
|
74
|
-
path = schema.path.to_s
|
|
75
|
-
|
|
76
|
-
# Check if this is from a manifest structure or a standalone EXPRESS file
|
|
77
|
-
case path
|
|
78
|
-
when %r{/resources/}
|
|
79
|
-
"resources"
|
|
80
|
-
when %r{/modules/}
|
|
81
|
-
"modules"
|
|
82
|
-
when %r{/business_object_models/}
|
|
83
|
-
"business_object_models"
|
|
84
|
-
when %r{/core_model/}
|
|
85
|
-
"core_model"
|
|
86
|
-
else
|
|
87
|
-
# standalone EXPRESS file not from a manifest structure
|
|
88
|
-
"standalone"
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
# rubocop:enable Metrics/MethodLength
|
|
92
|
-
|
|
93
65
|
# rubocop:disable Metrics/MethodLength
|
|
94
66
|
def create_zip_archive
|
|
95
67
|
require "zip"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "expressir"
|
|
4
|
+
|
|
5
|
+
module Suma
|
|
6
|
+
# Pre-built index for O(1) schema and element lookup.
|
|
7
|
+
# Build once from a parsed repo, then query by name.
|
|
8
|
+
class SchemaIndex
|
|
9
|
+
def initialize(repo)
|
|
10
|
+
@schemas_by_name = {}
|
|
11
|
+
@elements_by_schema = {}
|
|
12
|
+
|
|
13
|
+
repo.schemas.each do |schema|
|
|
14
|
+
key = schema.id.downcase
|
|
15
|
+
@schemas_by_name[key] = schema
|
|
16
|
+
@elements_by_schema[key] = build_element_index(schema)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def find_schema(name)
|
|
21
|
+
@schemas_by_name[name.downcase]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def find_element(schema_name, element_name)
|
|
25
|
+
elements = @elements_by_schema[schema_name.downcase]
|
|
26
|
+
elements&.[](element_name.downcase)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def build_element_index(schema)
|
|
32
|
+
index = {}
|
|
33
|
+
|
|
34
|
+
element_collections(schema).each do |collection|
|
|
35
|
+
collection&.each { |e| index[e.id.downcase] = e }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
index
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def element_collections(schema)
|
|
42
|
+
[
|
|
43
|
+
schema.entities,
|
|
44
|
+
schema.types,
|
|
45
|
+
schema.constants,
|
|
46
|
+
schema.functions,
|
|
47
|
+
schema.rules,
|
|
48
|
+
schema.procedures,
|
|
49
|
+
schema.subtype_constraints,
|
|
50
|
+
]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
module Suma
|
|
7
|
+
class SchemaManifestGenerator
|
|
8
|
+
YAML_FILE_EXTENSIONS = [".yaml", ".yml"].freeze
|
|
9
|
+
|
|
10
|
+
def initialize(metanorma_manifest_file, schema_manifest_file,
|
|
11
|
+
exclude_paths: nil)
|
|
12
|
+
@metanorma_manifest_file = File.expand_path(metanorma_manifest_file)
|
|
13
|
+
@schema_manifest_file = schema_manifest_file
|
|
14
|
+
@exclude_paths = exclude_paths
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def generate
|
|
18
|
+
validate_inputs
|
|
19
|
+
metanorma_data = load_yaml(@metanorma_manifest_file)
|
|
20
|
+
collection_files = metanorma_data["metanorma"]["source"]["files"]
|
|
21
|
+
manifest_files = load_manifest_files(collection_files)
|
|
22
|
+
all_schemas = load_project_schemas(manifest_files)
|
|
23
|
+
all_schemas["schemas"] = all_schemas["schemas"].sort.to_h
|
|
24
|
+
write_output(all_schemas)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def validate_inputs
|
|
30
|
+
unless File.exist?(@metanorma_manifest_file)
|
|
31
|
+
raise Errno::ENOENT,
|
|
32
|
+
"Specified file `#{@metanorma_manifest_file}` not found."
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
unless File.file?(@metanorma_manifest_file)
|
|
36
|
+
raise ArgumentError,
|
|
37
|
+
"Specified path `#{@metanorma_manifest_file}` is not a file."
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
[@metanorma_manifest_file, @schema_manifest_file].each do |file|
|
|
41
|
+
unless YAML_FILE_EXTENSIONS.include?(File.extname(file))
|
|
42
|
+
raise ArgumentError, "Specified file `#{file}` is not a YAML file."
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def load_yaml(file_path)
|
|
48
|
+
YAML.safe_load(File.read(file_path, encoding: "UTF-8"), aliases: true)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def load_manifest_files(collection_files)
|
|
52
|
+
collection_files.map do |c|
|
|
53
|
+
collection_data = load_yaml(c)
|
|
54
|
+
collection_data["manifest"]["docref"].map { |docref| docref["file"] }
|
|
55
|
+
end.flatten
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def load_project_schemas(manifest_files)
|
|
59
|
+
all_schemas = { "schemas" => {} }
|
|
60
|
+
|
|
61
|
+
manifest_files.each do |file|
|
|
62
|
+
schemas_file_path = File.expand_path(file.gsub("collection.yml",
|
|
63
|
+
"schemas.yaml"))
|
|
64
|
+
|
|
65
|
+
unless File.exist?(schemas_file_path)
|
|
66
|
+
Utils.log "Schemas file not found: #{schemas_file_path}"
|
|
67
|
+
next
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
schemas_data = load_yaml(schemas_file_path)
|
|
71
|
+
|
|
72
|
+
if schemas_data["schemas"]
|
|
73
|
+
schemas_data["schemas"] = fix_path(schemas_data, schemas_file_path)
|
|
74
|
+
all_schemas["schemas"].merge!(schemas_data["schemas"])
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
if @exclude_paths
|
|
78
|
+
all_schemas["schemas"].delete_if do |_key, value|
|
|
79
|
+
value["path"].match?(
|
|
80
|
+
Regexp.new(@exclude_paths.gsub("*", "(.*){1,999}")),
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
all_schemas
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def fix_path(schemas_data, schemas_file_path)
|
|
90
|
+
schema_manifest_path = File.expand_path(@schema_manifest_file, Dir.pwd)
|
|
91
|
+
|
|
92
|
+
schemas_data["schemas"].each do |key, value|
|
|
93
|
+
path_in_schema = File.expand_path(value["path"],
|
|
94
|
+
File.dirname(schemas_file_path))
|
|
95
|
+
|
|
96
|
+
fixed_path = Pathname.new(path_in_schema).relative_path_from(
|
|
97
|
+
Pathname.new(File.dirname(schema_manifest_path)),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
{ key => value.merge!("path" => fixed_path.to_s) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
schemas_data["schemas"]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def write_output(all_schemas)
|
|
107
|
+
output_path = File.expand_path(@schema_manifest_file)
|
|
108
|
+
Utils.log "Writing the Schemas YAML file to #{output_path}..."
|
|
109
|
+
File.write(output_path, all_schemas.to_yaml)
|
|
110
|
+
Utils.log "Writing the Schemas YAML file to #{output_path}...Done"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Suma
|
|
4
|
+
# Converts EXPRESS schema identifiers into human-readable display names.
|
|
5
|
+
#
|
|
6
|
+
# Naming is model-driven: the schema type (resource/module) and suffix
|
|
7
|
+
# determine how the identifier is formatted. Acronyms and numeric
|
|
8
|
+
# prefixes are preserved to match ISO 10303 conventions.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# SchemaNaming.display_name("topology_schema")
|
|
12
|
+
# # => "Topology"
|
|
13
|
+
# SchemaNaming.display_name("Activity_method_assignment_mim")
|
|
14
|
+
# # => "Activity Method Assignment (MIM)"
|
|
15
|
+
# SchemaNaming.display_name("aic_advanced_brep")
|
|
16
|
+
# # => "AIC Advanced Brep"
|
|
17
|
+
module SchemaNaming
|
|
18
|
+
# Acronyms preserved as uppercase during title-casing.
|
|
19
|
+
# Source: ISO 10303 naming conventions.
|
|
20
|
+
ACRONYMS = %w[
|
|
21
|
+
aic aec apu bom csg edraw id ifc pdf pld
|
|
22
|
+
xml xpdl 2d 3d
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
# Lowercase function words (ISO title-case convention).
|
|
26
|
+
LOWERCASE_WORDS = %w[a an and as for in of on or the to].freeze
|
|
27
|
+
|
|
28
|
+
# Suffixes stripped from the schema name before title-casing,
|
|
29
|
+
# mapped to the parenthesised label appended to the display name.
|
|
30
|
+
# A +nil+ label means the suffix is stripped silently.
|
|
31
|
+
SUFFIXES = {
|
|
32
|
+
"_arm" => "ARM",
|
|
33
|
+
"_mim" => "MIM",
|
|
34
|
+
"_bom" => "BOM",
|
|
35
|
+
"_schema" => nil,
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
class << self
|
|
39
|
+
# Produce a human-readable display name from a schema identifier.
|
|
40
|
+
#
|
|
41
|
+
# @param schema_id [String] the EXPRESS schema identifier
|
|
42
|
+
# @return [String] human-readable name
|
|
43
|
+
def display_name(schema_id)
|
|
44
|
+
base, label = decompose(schema_id)
|
|
45
|
+
title_cased = title_case(base)
|
|
46
|
+
label ? "#{title_cased} (#{label})" : title_cased
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Produce a prefixed display name with the schema category.
|
|
50
|
+
#
|
|
51
|
+
# @param schema_id [String] the EXPRESS schema identifier
|
|
52
|
+
# @param path [String, Pathname] the schema file path (for classification)
|
|
53
|
+
# @return [String] e.g. "Resource: Topology" or "Module: Activity (ARM)"
|
|
54
|
+
def prefixed_name(schema_id, path: nil)
|
|
55
|
+
type = ExpressSchema::Type.classify(id: schema_id, path: path)
|
|
56
|
+
prefix = category_prefix(type)
|
|
57
|
+
"#{prefix}: #{display_name(schema_id)}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Determine the category prefix for a schema type.
|
|
61
|
+
#
|
|
62
|
+
# @param type [Symbol] one of ExpressSchema::Type constants
|
|
63
|
+
# @return [String]
|
|
64
|
+
def category_prefix(type)
|
|
65
|
+
SchemaCategory.for_type(type).prefix
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Split a schema ID into (base_without_suffix, suffix_label_or_nil).
|
|
71
|
+
#
|
|
72
|
+
# @return [(String, String, nil)]
|
|
73
|
+
def decompose(schema_id)
|
|
74
|
+
SUFFIXES.each do |suffix, label|
|
|
75
|
+
if schema_id.end_with?(suffix)
|
|
76
|
+
return [schema_id.delete_suffix(suffix),
|
|
77
|
+
label]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
[schema_id, nil]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Title-case a snake_case identifier, preserving acronyms.
|
|
84
|
+
# The +tr+ collapses runs of underscores (leading, trailing, and
|
|
85
|
+
# consecutive) into spaces, which +split+ then treats as a single
|
|
86
|
+
# separator.
|
|
87
|
+
#
|
|
88
|
+
# @param name [String] snake_case identifier
|
|
89
|
+
# @return [String] title-cased name
|
|
90
|
+
def title_case(name)
|
|
91
|
+
words = name.tr("_", " ").split
|
|
92
|
+
words.each_with_index.map do |word, i|
|
|
93
|
+
capitalize_word(word, first: i.zero?)
|
|
94
|
+
end.join(" ")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Capitalise a single word, preserving acronyms and
|
|
98
|
+
# lowercasing function words (except when first in the name).
|
|
99
|
+
#
|
|
100
|
+
# @param word [String]
|
|
101
|
+
# @param first [Boolean] whether this is the first word
|
|
102
|
+
# @return [String]
|
|
103
|
+
def capitalize_word(word, first: false)
|
|
104
|
+
return word.upcase if ACRONYMS.include?(word.downcase)
|
|
105
|
+
return word.downcase if LOWERCASE_WORDS.include?(word.downcase) && !first
|
|
106
|
+
|
|
107
|
+
word.capitalize
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|