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.
@@ -1,168 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "thor"
4
- require "expressir"
5
4
 
6
5
  module Suma
7
6
  module Cli
7
+ # Deprecated: prefer +Cli::Validate#links+ (the +suma validate links+
8
+ # subcommand). This class is retained as a backwards-compat entry
9
+ # point — all orchestration now lives in +Suma::LinkValidation+.
8
10
  class ValidateLinks < Thor
9
11
  desc "extract_and_validate SCHEMAS_FILE DOCUMENTS_PATH [OUTPUT_FILE]",
10
12
  "Extract and validate express links without creating intermediate file"
11
13
  def extract_and_validate(schemas_file = "schemas-srl.yml",
12
- documents_path = "documents",
13
- output_file = "validation_results.txt")
14
- load_dependencies
15
- paths = prepare_file_paths(schemas_file, documents_path, output_file)
16
-
17
- schemas_config = load_schemas_config(paths[:schemas_file])
18
- exp_files = collect_schema_paths(schemas_config, paths[:schemas_file_rel])
19
- adoc_files = find_adoc_files(paths[:documents_path])
20
-
21
- all_files = adoc_files + exp_files
22
- display_file_counts(adoc_files, exp_files)
23
-
24
- links_by_file = extract_links(all_files)
25
-
26
- repo = load_express_schemas(schemas_config)
27
- index = SchemaIndex.new(repo)
28
- unresolved = LinkValidator.new(index).validate(links_by_file)
29
-
30
- write_validation_results(paths[:output_file], paths[:output_file_rel],
31
- unresolved, links_by_file)
32
- end
33
-
34
- private
35
-
36
- def load_dependencies
37
- require "expressir"
38
- require "ruby-progressbar"
39
- require "pathname"
40
- end
41
-
42
- def prepare_file_paths(schemas_file, documents_path, output_file)
43
- schemas_file_path = Pathname.new(schemas_file).expand_path
44
- documents_path_exp = Pathname.new(documents_path).expand_path
45
- output_file_path = Pathname.new(output_file).expand_path
46
-
47
- schemas_file_rel = Pathname.new(schemas_file_path).relative_path_from(Pathname.pwd).to_s
48
- documents_path_rel = Pathname.new(documents_path_exp).relative_path_from(Pathname.pwd).to_s
49
- output_file_rel = Pathname.new(output_file_path).relative_path_from(Pathname.pwd).to_s
50
-
51
- puts "Extracting and validating express links using schemas from #{schemas_file_rel}..."
52
- puts "Looking for documents in #{documents_path_rel}..."
53
-
54
- {
55
- schemas_file: schemas_file_path,
56
- schemas_file_rel: schemas_file_rel,
57
- documents_path: documents_path_exp,
58
- documents_path_rel: documents_path_rel,
59
- output_file: output_file_path,
60
- output_file_rel: output_file_rel,
61
- }
62
- end
63
-
64
- def load_schemas_config(schemas_file_path)
65
- schemas_config = Expressir::SchemaManifest.from_yaml(File.read(schemas_file_path))
66
- schemas_config.set_initial_path(schemas_file_path.to_s)
67
- schemas_config
68
- rescue StandardError => e
69
- raise Suma::Error, "Error loading schemas file: #{e.message}"
70
- end
71
-
72
- def collect_schema_paths(schemas_config, schemas_file_rel)
73
- exp_files = schemas_config.schemas.filter_map(&:path)
74
- puts "Found #{exp_files.size} EXPRESS schema files from #{schemas_file_rel}"
75
- exp_files
76
- end
77
-
78
- def find_adoc_files(documents_path)
79
- Dir.glob(documents_path.join("**", "*.adoc").to_s)
80
- end
81
-
82
- def display_file_counts(adoc_files, exp_files)
83
- puts "Found #{adoc_files.size} AsciiDoc files and #{exp_files.size} EXPRESS files"
84
- end
85
-
86
- def create_progress_bar(title, total)
87
- ProgressBar.create(
88
- title: title,
89
- total: total,
90
- format: "%t: [%B] %p%% %c/%C %e",
91
- progress_mark: "=",
92
- remainder_mark: " ",
93
- length: 80,
94
- )
95
- end
96
-
97
- def extract_links(files)
98
- links_by_file = {}
99
- link_count = 0
100
-
101
- progress = create_progress_bar("Processing files", files.size)
102
-
103
- files.each do |file|
104
- progress.increment
105
- begin
106
- content = File.read(file)
107
- express_links = content.scan(/<<express:([^,>]+)(?:,[^>]+)?>>/).flatten.uniq
108
-
109
- if express_links.any?
110
- links_by_file[file] = express_links
111
- link_count += express_links.size
112
- end
113
- rescue StandardError => e
114
- puts "\nWarning: Could not read file #{file}: #{e.message}"
115
- end
116
- end
117
-
118
- puts "\nExtracted #{link_count} unique express links from #{links_by_file.size} files"
119
- links_by_file
120
- end
121
-
122
- def load_express_schemas(schemas_config)
123
- schema_paths = {}
124
- schemas_config.schemas.each { |s| schema_paths[s.id] = s.path }
125
-
126
- puts "Loading #{schema_paths.size} EXPRESS schemas for validation..."
127
-
128
- loading_progress = create_progress_bar("Loading schemas", schema_paths.size)
129
-
130
- begin
131
- repo = Expressir::Express::Parser.from_files(schema_paths.values) do |filename, _schemas, error|
132
- loading_progress.increment
133
- puts "\nWarning: Error loading schema #{filename}: #{error.message}" if error
134
- end
135
-
136
- puts "Successfully loaded #{repo.schemas.size} schemas"
137
- repo
138
- rescue StandardError => e
139
- raise Suma::Error, "Error loading schemas: #{e.message}"
140
- end
141
- end
142
-
143
- def write_validation_results(output_file_path, output_file_rel,
144
- unresolved_links, links_by_file)
145
- total_links = links_by_file.values.sum(&:size)
146
-
147
- results = []
148
- results << "Validation complete. Checked #{total_links} links."
149
-
150
- if unresolved_links.empty?
151
- results << "✅ All links resolved successfully!"
152
- else
153
- results << "❌ Found #{unresolved_links.size} unresolved links:"
154
- unresolved_links.each do |issue|
155
- results << "#{issue.file}:#{issue.line} - <<express:#{issue.link}>> - #{issue.reason}"
156
- end
157
- end
158
-
159
- begin
160
- File.write(output_file_path, results.join("\n"))
161
- puts "Validation results written to #{output_file_rel}"
162
- rescue StandardError => e
163
- puts "Error writing to output file: #{e.message}"
164
- puts results
165
- end
14
+ documents_path = "documents",
15
+ output_file = "validation_results.txt")
16
+ result = LinkValidation.new(
17
+ schemas_file: schemas_file,
18
+ documents_path: documents_path,
19
+ output_file: output_file,
20
+ ).call
21
+ puts LinkValidation.generate_summary(result)
166
22
  end
167
23
  end
168
24
  end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Suma
4
+ # Reformats EXPRESS source into suma's canonical form: comment blocks
5
+ # (the +(*"...\n*)+ remarks that EXPRESS uses for documentation) are
6
+ # extracted from their inline positions and appended to the end of
7
+ # the file, and excess blank lines are collapsed.
8
+ #
9
+ # Pure content transformation — no I/O, no Thor, no filesystem. The
10
+ # CLI adapter handles reading from and writing to disk.
11
+ #
12
+ # The transform is idempotent: reformatting an already-reformatted
13
+ # document returns +changed?: false+.
14
+ #
15
+ # Comment extraction uses +String#index+ rather than a single m-mode
16
+ # regex. The original +/\(\*"(.*?)\n\*\)/m+ is correct functionally
17
+ # but is polynomial-time on adversarial input (CodeQL
18
+ # +rb/polynomial-redos+). Scanning for the start marker, then for
19
+ # the next terminator, is unambiguously linear.
20
+ module ExpressReformatter
21
+ Result = Struct.new(:content, :changed?, keyword_init: true) do
22
+ def content_or_nil
23
+ changed? ? content : nil
24
+ end
25
+ end
26
+
27
+ COMMENT_START = '(*"'
28
+ COMMENT_END = "\n*)"
29
+ BLANK_RUN_PATTERN = /(\n\n+)/
30
+ NEWLINE_RUN_PATTERN = /(\n+)/
31
+
32
+ module_function
33
+
34
+ def call(content)
35
+ comments = extract_comments(content)
36
+ return Result.new(content: content, changed?: false) if comments.empty?
37
+
38
+ without_comments = strip_comments(content)
39
+ new_comments = comments.map { |c| "#{COMMENT_START}#{c}#{COMMENT_END}" }
40
+ .join("\n\n")
41
+ new_content = "#{without_comments}\n\n#{new_comments}\n"
42
+ new_content = new_content.gsub(BLANK_RUN_PATTERN, "\n\n")
43
+
44
+ changed = normalised_compare(content, new_content) != 0
45
+ Result.new(content: new_content, changed?: changed)
46
+ end
47
+
48
+ def extract_comments(content)
49
+ comments = []
50
+ pos = 0
51
+ while (start_idx = content.index(COMMENT_START, pos))
52
+ end_idx = content.index(COMMENT_END, start_idx + COMMENT_START.length)
53
+ break unless end_idx
54
+
55
+ comments << content[(start_idx + COMMENT_START.length)...end_idx]
56
+ pos = end_idx + COMMENT_END.length
57
+ end
58
+ comments
59
+ end
60
+
61
+ def strip_comments(content)
62
+ return content unless content.index(COMMENT_START, 0)
63
+
64
+ stripped = +""
65
+ last_end = 0
66
+ each_comment_range(content) do |range|
67
+ stripped << content[range[:pre_start]...range[:start]]
68
+ last_end = range[:end_exclusive]
69
+ end
70
+ stripped << content[last_end..]
71
+ stripped.force_encoding(content.encoding)
72
+ end
73
+
74
+ def each_comment_range(content)
75
+ return enum_for(:each_comment_range, content) unless block_given?
76
+
77
+ pos = 0
78
+ while (start_idx = content.index(COMMENT_START, pos))
79
+ end_idx = content.index(COMMENT_END, start_idx + COMMENT_START.length)
80
+ break unless end_idx
81
+
82
+ yield pre_start: pos, start: start_idx,
83
+ end_exclusive: end_idx + COMMENT_END.length
84
+ pos = end_idx + COMMENT_END.length
85
+ end
86
+ end
87
+
88
+ def normalised_compare(left, right)
89
+ left.gsub(NEWLINE_RUN_PATTERN, "\n") <=> right.gsub(NEWLINE_RUN_PATTERN, "\n")
90
+ end
91
+
92
+ private_class_method :normalised_compare
93
+ end
94
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "expressir"
5
+
6
+ module Suma
7
+ # Deep module behind the EXPRESS cross-reference validation pipeline.
8
+ #
9
+ # Interface: paths in, +Result+ out. The CLI is a thin adapter that
10
+ # constructs this object and prints the result.
11
+ #
12
+ # Owns: loading the schemas manifest, discovering + reading .adoc and
13
+ # .exp files, extracting express cross-reference links, loading parsed
14
+ # schemas into a +SchemaIndex+, delegating to +LinkValidator+ for the
15
+ # actual resolution, and writing the summary file.
16
+ #
17
+ # Does not own: presentation (use +LinkValidation.generate_summary+),
18
+ # command-line argument parsing (the CLI adapter does that), or the
19
+ # link-resolution rules themselves (that is +LinkValidator+'s job).
20
+ class LinkValidation
21
+ EXPRESS_LINK_PATTERN = /<<express:([^,>]+)(?:,[^>]+)?>>/
22
+
23
+ attr_reader :schemas_file, :documents_path, :output_file,
24
+ :progress, :logger
25
+
26
+ def initialize(schemas_file:, documents_path:, output_file:,
27
+ progress: NullProgress.new, logger: Utils)
28
+ @schemas_file = Pathname.new(schemas_file).expand_path
29
+ @documents_path = Pathname.new(documents_path).expand_path
30
+ @output_file = output_file && Pathname.new(output_file).expand_path
31
+ @progress = progress
32
+ @logger = logger
33
+ end
34
+
35
+ def call
36
+ config = load_schemas_config
37
+ exp_files = collect_schema_paths(config)
38
+ adoc_files = find_adoc_files
39
+ links_by_file = extract_links(adoc_files + exp_files)
40
+ unresolved = validate_links(config, links_by_file)
41
+ result = Result.new(
42
+ adoc_count: adoc_files.size,
43
+ exp_count: exp_files.size,
44
+ total_links: links_by_file.values.sum(&:size),
45
+ unresolved: unresolved,
46
+ )
47
+ write_summary(result) if output_file
48
+ result
49
+ end
50
+
51
+ def self.generate_summary(result)
52
+ lines = []
53
+ lines << "Validation complete. Checked #{result.total_links} links."
54
+ if result.success?
55
+ lines << "✅ All links resolved successfully!"
56
+ else
57
+ lines << "❌ Found #{result.unresolved.size} unresolved links:"
58
+ result.unresolved.each { |issue| lines << format_issue(issue) }
59
+ end
60
+ lines.join("\n")
61
+ end
62
+
63
+ def self.format_issue(issue)
64
+ "#{issue.file}:#{issue.line} - " \
65
+ "<<express:#{issue.link}>> - #{issue.reason}"
66
+ end
67
+ private_class_method :format_issue
68
+
69
+ # Default no-op progress adapter. Callers that want a real progress
70
+ # bar pass an object responding to +#start(title, total)+ and
71
+ # +#increment+; this satisfies the same interface without forcing
72
+ # the dependency.
73
+ class NullProgress
74
+ def start(_title, _total); end
75
+
76
+ def increment; end
77
+ end
78
+
79
+ Result = Struct.new(
80
+ :adoc_count,
81
+ :exp_count,
82
+ :total_links,
83
+ :unresolved,
84
+ keyword_init: true,
85
+ ) do
86
+ def success?
87
+ unresolved.empty?
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ def load_schemas_config
94
+ Expressir::SchemaManifest.from_yaml(File.read(schemas_file))
95
+ .tap { |c| c.set_initial_path(schemas_file.to_s) }
96
+ rescue StandardError => e
97
+ raise Error, "Error loading schemas file: #{e.message}"
98
+ end
99
+
100
+ def collect_schema_paths(schemas_config)
101
+ schemas_config.schemas.filter_map(&:path)
102
+ end
103
+
104
+ def find_adoc_files
105
+ Dir.glob(documents_path.join("**", "*.adoc").to_s)
106
+ end
107
+
108
+ def extract_links(files)
109
+ links_by_file = {}
110
+ progress.start("Processing files", files.size)
111
+ files.each do |file|
112
+ links = extract_links_from_file(file)
113
+ links_by_file[file] = links if links&.any?
114
+ end
115
+ links_by_file
116
+ end
117
+
118
+ def extract_links_from_file(file)
119
+ progress.increment
120
+ content = File.read(file)
121
+ content.scan(EXPRESS_LINK_PATTERN).flatten.uniq
122
+ rescue StandardError => e
123
+ logger.log "Warning: Could not read file #{file}: #{e.message}"
124
+ nil
125
+ end
126
+
127
+ def validate_links(schemas_config, links_by_file)
128
+ paths_by_id = schemas_config.schemas.to_h { |s| [s.id, s.path] }
129
+ progress.start("Loading schemas", paths_by_id.size)
130
+ repo = Expressir::Express::Parser.from_files(paths_by_id.values) do |*_args|
131
+ progress.increment
132
+ end
133
+ index = SchemaIndex.new(repo)
134
+ LinkValidator.new(index).validate(links_by_file)
135
+ end
136
+
137
+ def write_summary(result)
138
+ FileUtils.mkdir_p(output_file.dirname)
139
+ File.write(output_file, self.class.generate_summary(result))
140
+ rescue StandardError => e
141
+ logger.log "Error writing to output file: #{e.message}"
142
+ end
143
+ end
144
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "expressir"
4
+ require "suma/link_validation"
4
5
 
5
6
  module Suma
6
7
  LinkValidationResult = Struct.new(:file, :line, :link, :reason,
@@ -28,7 +29,7 @@ module Suma
28
29
  content = File.read(file)
29
30
  index = {}
30
31
  content.lines.each_with_index do |line, idx|
31
- line.scan(/<<express:([^,>]+)(?:,[^>]+)?>>/).flatten.each do |link|
32
+ line.scan(LinkValidation::EXPRESS_LINK_PATTERN).flatten.each do |link|
32
33
  index[link] ||= idx
33
34
  end
34
35
  end
@@ -40,7 +40,7 @@ module Suma
40
40
  finalize
41
41
 
42
42
  exporter = SchemaExporter.new(
43
- schemas: @config.schemas,
43
+ schemas: schemas.values,
44
44
  output_path: @output_path_schemas,
45
45
  options: { annotations: false },
46
46
  )
@@ -3,8 +3,19 @@
3
3
  require "fileutils"
4
4
 
5
5
  module Suma
6
- # SchemaExporter exports EXPRESS schemas from a manifest
7
- # with configurable options for annotations and ZIP packaging
6
+ # Exports EXPRESS schemas to a directory, with optional ZIP packaging.
7
+ #
8
+ # Pure sink: the exporter accepts already-loaded +Suma::ExpressSchema+
9
+ # instances and writes their content to disk. Construction of those
10
+ # instances (with the right +output_path+ and +is_standalone_file+
11
+ # flags) is the caller's responsibility — the exporter does not
12
+ # reach across the seam to inspect manifest entries or classify
13
+ # schema types itself.
14
+ #
15
+ # This is a deep module: a small interface (one +export+ method, one
16
+ # option hash) backed by save_exp + zip packaging. The CLI and
17
+ # SchemaCollection adapters construct ExpressSchema instances; the
18
+ # exporter never inspects their shape.
8
19
  class SchemaExporter
9
20
  attr_reader :schemas, :output_path, :options
10
21
 
@@ -35,30 +46,7 @@ module Suma
35
46
 
36
47
  def export_to_directory(schemas)
37
48
  schemas.each do |schema|
38
- export_single_schema(schema)
39
- end
40
- end
41
-
42
- def export_single_schema(schema)
43
- is_standalone = !schema.is_a?(Expressir::SchemaManifestEntry)
44
- schema_output_path = determine_output_path(schema, is_standalone)
45
-
46
- express_schema = ExpressSchema.new(
47
- id: schema.id,
48
- path: schema.path.to_s,
49
- output_path: schema_output_path,
50
- is_standalone_file: is_standalone,
51
- )
52
-
53
- express_schema.save_exp(with_annotations: options[:annotations])
54
- end
55
-
56
- def determine_output_path(schema, is_standalone)
57
- if is_standalone
58
- output_path.to_s
59
- else
60
- category = SchemaCategory.for_schema(id: schema.id, path: schema.path)
61
- output_path.join(category.directory).to_s
49
+ schema.save_exp(with_annotations: options[:annotations])
62
50
  end
63
51
  end
64
52
 
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "svg_conform"
4
+
5
+ module Suma
6
+ module SvgQuality
7
+ # Deep module behind the SVG-quality seam: takes paths in, returns
8
+ # +Report+ / +BatchReport+ out. Owns validator construction and
9
+ # per-file result capture. Does not own file discovery, sorting,
10
+ # filtering, or presentation — those stay in the CLI adapter.
11
+ #
12
+ # The progress adapter is injected: pass any object responding to
13
+ # +#call(index, total, report)+. +NullProgress+ is the default
14
+ # no-op; the CLI passes a lambda that writes to +$stderr+.
15
+ class Scanner
16
+ DEFAULT_PROFILE = :metanorma
17
+
18
+ attr_reader :profile, :progress
19
+
20
+ def initialize(profile: DEFAULT_PROFILE,
21
+ progress: NullProgress.new)
22
+ @profile = profile
23
+ @progress = progress
24
+ end
25
+
26
+ def scan(paths)
27
+ validator = build_validator
28
+ reports = paths.each_with_index.map do |path, index|
29
+ scan_one(validator, path, index, paths.size)
30
+ end
31
+ BatchReport.new(reports)
32
+ end
33
+
34
+ def scan_file(path)
35
+ Report.new(path.to_s, build_validator.validate_file(path.to_s,
36
+ profile: profile))
37
+ end
38
+
39
+ # Default no-op progress adapter. Real progress reporters are
40
+ # passed by the caller; this satisfies the same interface so the
41
+ # scanner can be invoked from specs without forcing the
42
+ # dependency.
43
+ class NullProgress
44
+ def call(_index, _total, _report); end
45
+ end
46
+
47
+ private
48
+
49
+ def build_validator
50
+ SvgConform::Validator.new
51
+ end
52
+
53
+ def scan_one(validator, path, index, total)
54
+ result = validator.validate_file(path.to_s, profile: profile)
55
+ report = Report.new(path.to_s, result)
56
+ progress.call(index, total, report)
57
+ report
58
+ end
59
+ end
60
+ end
61
+ end
@@ -6,6 +6,7 @@ module Suma
6
6
  module SvgQuality
7
7
  autoload :Report, "suma/svg_quality/report"
8
8
  autoload :BatchReport, "suma/svg_quality/batch_report"
9
+ autoload :Scanner, "suma/svg_quality/scanner"
9
10
 
10
11
  module QualityTiers
11
12
  CRITICAL = { name: :critical, min_errors: 200, emoji: "💥" }.freeze
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Suma
4
+ # Term-extractor-specific classification of an EXPRESS schema.
5
+ #
6
+ # Bridges ExpressSchema::Type (the canonical classification shared
7
+ # across the codebase) and the Glossarist-specific labels
8
+ # TermExtractor emits: the domain string ("application module" /
9
+ # "resource") that goes into a concept's +domain+ field, and the
10
+ # entity-type URN term used in generated concept definitions.
11
+ #
12
+ # The mapping is data — a frozen Hash keyed by ExpressSchema::Type
13
+ # symbol — so adding a new schema type is a one-line addition to
14
+ # BY_TYPE (open/closed principle). The previous implementation
15
+ # switched on string keys in three separate places; this consolidates
16
+ # them into one source of truth.
17
+ class TermClassification
18
+ attr_reader :type, :domain_label, :entity_term, :entity_display
19
+
20
+ def initialize(type:, domain_label:, entity_term:, entity_display:)
21
+ @type = type
22
+ @domain_label = domain_label
23
+ @entity_term = entity_term
24
+ @entity_display = entity_display
25
+ freeze
26
+ end
27
+
28
+ def domain_for(schema_id)
29
+ "#{domain_label}: #{schema_id}"
30
+ end
31
+
32
+ BY_TYPE = {
33
+ ExpressSchema::Type::RESOURCE => new(
34
+ type: ExpressSchema::Type::RESOURCE,
35
+ domain_label: "resource",
36
+ entity_term: "express-language.entity_data_type",
37
+ entity_display: "entity data type",
38
+ ),
39
+ ExpressSchema::Type::MODULE_ARM => new(
40
+ type: ExpressSchema::Type::MODULE_ARM,
41
+ domain_label: "application module",
42
+ entity_term: "general.application_object",
43
+ entity_display: "application object",
44
+ ),
45
+ ExpressSchema::Type::MODULE_MIM => new(
46
+ type: ExpressSchema::Type::MODULE_MIM,
47
+ domain_label: "application module",
48
+ entity_term: "express-language.entity_data_type",
49
+ entity_display: "entity data type",
50
+ ),
51
+ ExpressSchema::Type::BUSINESS_OBJECT_MODEL => new(
52
+ type: ExpressSchema::Type::BUSINESS_OBJECT_MODEL,
53
+ domain_label: "resource",
54
+ entity_term: "express-language.entity_data_type",
55
+ entity_display: "entity data type",
56
+ ),
57
+ ExpressSchema::Type::CORE_MODEL => new(
58
+ type: ExpressSchema::Type::CORE_MODEL,
59
+ domain_label: "resource",
60
+ entity_term: "express-language.entity_data_type",
61
+ entity_display: "entity data type",
62
+ ),
63
+ ExpressSchema::Type::STANDALONE => new(
64
+ type: ExpressSchema::Type::STANDALONE,
65
+ domain_label: "resource",
66
+ entity_term: "express-language.entity_data_type",
67
+ entity_display: "entity data type",
68
+ ),
69
+ }.freeze
70
+
71
+ def self.for_schema(id:, path:)
72
+ type = ExpressSchema::Type.classify(id: id, path: path)
73
+ BY_TYPE.fetch(type) do |t|
74
+ raise Error, "[suma] no term classification for type #{t.inspect}"
75
+ end
76
+ end
77
+ end
78
+ end