suma 0.5.7 → 0.5.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 +4 -4
- data/.github/workflows/nightly-benchmark.yml +25 -0
- data/.github/workflows/release-provenance.yml +41 -0
- data/.rubocop_todo.yml +84 -0
- data/CLAUDE.md +44 -0
- data/README.adoc +136 -11
- data/Rakefile +13 -1
- data/bin/benchmark_corpus +138 -0
- data/bin/release_provenance +40 -0
- data/lib/suma/build_plan.rb +71 -0
- data/lib/suma/change_classification.rb +37 -0
- data/lib/suma/change_impact.rb +61 -0
- data/lib/suma/change_manifest.rb +147 -0
- data/lib/suma/cli/build.rb +23 -0
- data/lib/suma/cli/compare.rb +49 -2
- data/lib/suma/cli/core.rb +83 -0
- data/lib/suma/cli/export.rb +140 -10
- data/lib/suma/cli/extract_terms.rb +11 -0
- data/lib/suma/cli/generate_register.rb +12 -0
- data/lib/suma/cli/resolve.rb +45 -0
- data/lib/suma/cli/validate.rb +90 -0
- data/lib/suma/cli.rb +1 -0
- data/lib/suma/closure_validator.rb +317 -0
- data/lib/suma/corpus_graph.rb +141 -0
- data/lib/suma/express_schema.rb +22 -28
- data/lib/suma/null_cache.rb +4 -0
- data/lib/suma/owl_projection.rb +611 -0
- data/lib/suma/processor.rb +55 -6
- data/lib/suma/provenance.rb +159 -0
- data/lib/suma/publication.rb +108 -0
- data/lib/suma/register_manifest_generator.rb +76 -8
- data/lib/suma/schema_cache.rb +72 -13
- data/lib/suma/schema_category.rb +31 -2
- data/lib/suma/schema_collection.rb +58 -5
- data/lib/suma/schema_exporter.rb +5 -5
- data/lib/suma/schema_naming.rb +29 -33
- data/lib/suma/schema_profile.rb +134 -0
- data/lib/suma/term_classification.rb +9 -2
- data/lib/suma/term_extractor.rb +76 -35
- data/lib/suma/urn_resolver.rb +368 -0
- data/lib/suma/version.rb +1 -1
- data/lib/suma/worker_pool.rb +62 -0
- data/lib/suma.rb +12 -0
- metadata +19 -2
data/lib/suma/schema_exporter.rb
CHANGED
|
@@ -42,11 +42,14 @@ module Suma
|
|
|
42
42
|
annotations: false,
|
|
43
43
|
create_zip: false,
|
|
44
44
|
structure: :preserve,
|
|
45
|
+
jobs: 1,
|
|
45
46
|
}
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
def export_to_directory(schemas)
|
|
49
|
-
|
|
50
|
+
WorkerPool.new(size: options[:jobs]).map(schemas) do |schema|
|
|
51
|
+
export_single_schema(schema)
|
|
52
|
+
end
|
|
50
53
|
end
|
|
51
54
|
|
|
52
55
|
# Reuse cached plain/annotated output when the schema source is unchanged,
|
|
@@ -77,10 +80,7 @@ module Suma
|
|
|
77
80
|
# (via the +:cache_dir+ option or the SUMA_SCHEMA_CACHE_DIR environment
|
|
78
81
|
# variable), otherwise a null cache (caching disabled).
|
|
79
82
|
def build_cache
|
|
80
|
-
|
|
81
|
-
return NullCache.new if directory.nil? || directory.empty?
|
|
82
|
-
|
|
83
|
-
SchemaCache.new(directory)
|
|
83
|
+
SchemaCache.build(options)
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
# rubocop:disable-next Metrics/MethodLength
|
data/lib/suma/schema_naming.rb
CHANGED
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
module Suma
|
|
4
4
|
# Converts EXPRESS schema identifiers into human-readable display names.
|
|
5
5
|
#
|
|
6
|
-
# Naming is
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# Naming is profile-driven: the profile declares the suffixes
|
|
7
|
+
# stripped from the identifier (with their parenthesised labels) and
|
|
8
|
+
# the acronyms preserved during title-casing. The ISO 10303 profile
|
|
9
|
+
# reproduces the historical behavior — ARM/MIM/BOM labels and the ISO
|
|
10
|
+
# acronym set; the generic profile strips nothing and applies plain
|
|
11
|
+
# title-casing.
|
|
9
12
|
#
|
|
10
13
|
# @example
|
|
11
14
|
# SchemaNaming.display_name("topology_schema")
|
|
@@ -15,34 +18,19 @@ module Suma
|
|
|
15
18
|
# SchemaNaming.display_name("aic_advanced_brep")
|
|
16
19
|
# # => "AIC Advanced Brep"
|
|
17
20
|
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
21
|
# Lowercase function words (ISO title-case convention).
|
|
26
22
|
LOWERCASE_WORDS = %w[a an and as for in of on or the to].freeze
|
|
27
23
|
|
|
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
24
|
class << self
|
|
39
25
|
# Produce a human-readable display name from a schema identifier.
|
|
40
26
|
#
|
|
41
27
|
# @param schema_id [String] the EXPRESS schema identifier
|
|
28
|
+
# @param profile [SchemaProfile, Symbol, String, nil] schema profile
|
|
42
29
|
# @return [String] human-readable name
|
|
43
|
-
def display_name(schema_id)
|
|
44
|
-
|
|
45
|
-
|
|
30
|
+
def display_name(schema_id, profile: nil)
|
|
31
|
+
profile_obj = SchemaProfile.fetch(profile)
|
|
32
|
+
base, label = decompose(schema_id, profile_obj.suffix_labels)
|
|
33
|
+
title_cased = title_case(base, profile_obj)
|
|
46
34
|
label ? "#{title_cased} (#{label})" : title_cased
|
|
47
35
|
end
|
|
48
36
|
|
|
@@ -50,11 +38,13 @@ module Suma
|
|
|
50
38
|
#
|
|
51
39
|
# @param schema_id [String] the EXPRESS schema identifier
|
|
52
40
|
# @param path [String, Pathname] the schema file path (for classification)
|
|
41
|
+
# @param profile [SchemaProfile, Symbol, String, nil] schema profile
|
|
53
42
|
# @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
|
|
43
|
+
def prefixed_name(schema_id, path: nil, profile: nil)
|
|
44
|
+
type = ExpressSchema::Type.classify(id: schema_id, path: path,
|
|
45
|
+
profile: profile)
|
|
56
46
|
prefix = category_prefix(type)
|
|
57
|
-
"#{prefix}: #{display_name(schema_id)}"
|
|
47
|
+
"#{prefix}: #{display_name(schema_id, profile: profile)}"
|
|
58
48
|
end
|
|
59
49
|
|
|
60
50
|
# Determine the category prefix for a schema type.
|
|
@@ -69,9 +59,11 @@ module Suma
|
|
|
69
59
|
|
|
70
60
|
# Split a schema ID into (base_without_suffix, suffix_label_or_nil).
|
|
71
61
|
#
|
|
62
|
+
# @param schema_id [String]
|
|
63
|
+
# @param suffix_labels [Hash{String => String, nil}]
|
|
72
64
|
# @return [(String, String, nil)]
|
|
73
|
-
def decompose(schema_id)
|
|
74
|
-
|
|
65
|
+
def decompose(schema_id, suffix_labels)
|
|
66
|
+
suffix_labels.each do |suffix, label|
|
|
75
67
|
if schema_id.end_with?(suffix)
|
|
76
68
|
return [schema_id.delete_suffix(suffix),
|
|
77
69
|
label]
|
|
@@ -86,11 +78,12 @@ module Suma
|
|
|
86
78
|
# separator.
|
|
87
79
|
#
|
|
88
80
|
# @param name [String] snake_case identifier
|
|
81
|
+
# @param acronyms [Array<String>] lowercased words kept uppercase
|
|
89
82
|
# @return [String] title-cased name
|
|
90
|
-
def title_case(name)
|
|
83
|
+
def title_case(name, profile_obj)
|
|
91
84
|
words = name.tr("_", " ").split
|
|
92
85
|
words.each_with_index.map do |word, i|
|
|
93
|
-
capitalize_word(word, first: i.zero?)
|
|
86
|
+
capitalize_word(word, profile_obj: profile_obj, first: i.zero?)
|
|
94
87
|
end.join(" ")
|
|
95
88
|
end
|
|
96
89
|
|
|
@@ -98,11 +91,14 @@ module Suma
|
|
|
98
91
|
# lowercasing function words (except when first in the name).
|
|
99
92
|
#
|
|
100
93
|
# @param word [String]
|
|
94
|
+
# @param acronyms [Array<String>]
|
|
101
95
|
# @param first [Boolean] whether this is the first word
|
|
102
96
|
# @return [String]
|
|
103
|
-
def capitalize_word(word, first: false)
|
|
104
|
-
|
|
105
|
-
return word.
|
|
97
|
+
def capitalize_word(word, profile_obj:, first: false)
|
|
98
|
+
lower = word.downcase
|
|
99
|
+
return word.upcase if profile_obj.acronyms.include?(lower)
|
|
100
|
+
return word.upcase if profile_obj.acronym_patterns.any? { |re| re.match?(lower) }
|
|
101
|
+
return word.downcase if LOWERCASE_WORDS.include?(lower) && !first
|
|
106
102
|
|
|
107
103
|
word.capitalize
|
|
108
104
|
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Suma
|
|
4
|
+
# Declares how a schema collection maps schema identifiers and paths
|
|
5
|
+
# to schema types, and how identifiers become display names.
|
|
6
|
+
#
|
|
7
|
+
# Suma works with any schema collection: ISO 10303 conventions (the
|
|
8
|
+
# `_arm`/`_mim` suffixes, the `resources/`+`modules/` directory tree,
|
|
9
|
+
# the ARM/MIM display labels) are one profile among many, not
|
|
10
|
+
# hardcoded behavior. A new collection's conventions are a new profile
|
|
11
|
+
# declaration — no changes to classification, naming, categories, or
|
|
12
|
+
# term extraction.
|
|
13
|
+
#
|
|
14
|
+
# Rules are ordered: the first matching rule classifies the schema.
|
|
15
|
+
# In the ISO 10303 profile the `/applications/` path rule precedes the
|
|
16
|
+
# suffix rules so an AP publication's `_arm`/`_mim` schemas classify as
|
|
17
|
+
# application protocols, while the same suffixes under `modules/`
|
|
18
|
+
# classify as before.
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# SchemaProfile.fetch("iso10303").classify(
|
|
22
|
+
# id: "ap242_foo_mim", path: "applications/ap242/foo.exp"
|
|
23
|
+
# ) # => :application_protocol
|
|
24
|
+
# SchemaProfile.fetch("generic").classify(id: "x", path: "y.exp")
|
|
25
|
+
# # => :standalone
|
|
26
|
+
class SchemaProfile
|
|
27
|
+
# A single classification rule: +kind+ is :suffix (matched against the
|
|
28
|
+
# lowercased schema identifier) or :path (matched against the schema
|
|
29
|
+
# file path); +pattern+ is the string to match; +type+ is the
|
|
30
|
+
# ExpressSchema::Type symbol the rule produces.
|
|
31
|
+
Rule = Struct.new(:kind, :pattern, :type) do
|
|
32
|
+
def match(schema_id, schema_path)
|
|
33
|
+
case kind
|
|
34
|
+
when :suffix
|
|
35
|
+
schema_id&.downcase&.end_with?(pattern)
|
|
36
|
+
when :path
|
|
37
|
+
schema_path.to_s.include?(pattern)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
attr_reader :id, :rules, :suffix_labels, :acronyms,
|
|
43
|
+
:acronym_patterns, :category_ids
|
|
44
|
+
|
|
45
|
+
# (a data-declaring initializer; six keywords by design)
|
|
46
|
+
def initialize(id:, rules: [], suffix_labels: {}, acronyms: [], # rubocop:disable Metrics/ParameterLists
|
|
47
|
+
acronym_patterns: [], category_ids: %w[other])
|
|
48
|
+
@id = id
|
|
49
|
+
@rules = rules.freeze
|
|
50
|
+
@suffix_labels = suffix_labels.freeze
|
|
51
|
+
@acronyms = acronyms.freeze
|
|
52
|
+
@acronym_patterns = acronym_patterns.freeze
|
|
53
|
+
@category_ids = category_ids.freeze
|
|
54
|
+
freeze
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Classify a schema under this profile.
|
|
58
|
+
#
|
|
59
|
+
# @param id [String, nil] schema identifier
|
|
60
|
+
# @param path [String, Pathname, nil] schema file path
|
|
61
|
+
# @return [Symbol] an ExpressSchema::Type value; +:standalone+ when no
|
|
62
|
+
# rule matches
|
|
63
|
+
def classify(id:, path:)
|
|
64
|
+
rules.each do |rule|
|
|
65
|
+
return rule.type if rule.match(id, path)
|
|
66
|
+
end
|
|
67
|
+
ExpressSchema::Type::STANDALONE
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The default (ISO 10303) profile.
|
|
71
|
+
#
|
|
72
|
+
# @return [SchemaProfile]
|
|
73
|
+
def self.default
|
|
74
|
+
fetch("iso10303")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Look up a registered profile by id; +nil+ yields the default.
|
|
78
|
+
#
|
|
79
|
+
# @param id [String, Symbol, SchemaProfile, nil]
|
|
80
|
+
# @return [SchemaProfile]
|
|
81
|
+
# @raise [Suma::Error] on an unknown profile id
|
|
82
|
+
def self.fetch(id)
|
|
83
|
+
return id if id.is_a?(SchemaProfile)
|
|
84
|
+
return default if id.nil?
|
|
85
|
+
|
|
86
|
+
REGISTRY.fetch(id.to_s) do
|
|
87
|
+
raise Error, "[suma] unknown schema profile #{id.inspect} " \
|
|
88
|
+
"(registered: #{REGISTRY.keys.sort.join(', ')})"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
ISO_10303 = new(
|
|
93
|
+
id: "iso10303",
|
|
94
|
+
rules: [
|
|
95
|
+
Rule.new(:path, "/applications/", :application_protocol),
|
|
96
|
+
Rule.new(:suffix, "_arm", :module_arm),
|
|
97
|
+
Rule.new(:suffix, "_mim", :module_mim),
|
|
98
|
+
Rule.new(:suffix, "_bom", :business_object_model),
|
|
99
|
+
Rule.new(:path, "/resources/", :resource),
|
|
100
|
+
Rule.new(:path, "/modules/", :module_arm),
|
|
101
|
+
Rule.new(:path, "/core_model/", :core_model),
|
|
102
|
+
],
|
|
103
|
+
suffix_labels: {
|
|
104
|
+
"_arm" => "ARM",
|
|
105
|
+
"_mim" => "MIM",
|
|
106
|
+
"_bom" => "BOM",
|
|
107
|
+
"_schema" => nil,
|
|
108
|
+
},
|
|
109
|
+
acronyms: %w[
|
|
110
|
+
aic aec ap apu bom csg edraw id ifc pdf pld
|
|
111
|
+
xml xpdl 2d 3d
|
|
112
|
+
],
|
|
113
|
+
# AP publications glue the number to the prefix: ap203, ap242, ...
|
|
114
|
+
acronym_patterns: [/\Aap\d+\z/],
|
|
115
|
+
category_ids: %w[
|
|
116
|
+
resources application_protocols modules
|
|
117
|
+
business_object_models core_model other
|
|
118
|
+
],
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
GENERIC = new(
|
|
122
|
+
id: "generic",
|
|
123
|
+
rules: [],
|
|
124
|
+
suffix_labels: {},
|
|
125
|
+
acronyms: [],
|
|
126
|
+
category_ids: %w[other],
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
REGISTRY = {
|
|
130
|
+
"iso10303" => ISO_10303,
|
|
131
|
+
"generic" => GENERIC,
|
|
132
|
+
}.freeze
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -36,6 +36,12 @@ module Suma
|
|
|
36
36
|
entity_term: "express-language.entity_data_type",
|
|
37
37
|
entity_display: "entity data type",
|
|
38
38
|
),
|
|
39
|
+
ExpressSchema::Type::APPLICATION_PROTOCOL => new(
|
|
40
|
+
type: ExpressSchema::Type::APPLICATION_PROTOCOL,
|
|
41
|
+
domain_label: "application protocol",
|
|
42
|
+
entity_term: "express-language.entity_data_type",
|
|
43
|
+
entity_display: "entity data type",
|
|
44
|
+
),
|
|
39
45
|
ExpressSchema::Type::MODULE_ARM => new(
|
|
40
46
|
type: ExpressSchema::Type::MODULE_ARM,
|
|
41
47
|
domain_label: "application module",
|
|
@@ -68,8 +74,9 @@ module Suma
|
|
|
68
74
|
),
|
|
69
75
|
}.freeze
|
|
70
76
|
|
|
71
|
-
def self.for_schema(id:, path:)
|
|
72
|
-
type = ExpressSchema::Type.classify(id: id, path: path
|
|
77
|
+
def self.for_schema(id:, path:, profile: nil)
|
|
78
|
+
type = ExpressSchema::Type.classify(id: id, path: path,
|
|
79
|
+
profile: profile)
|
|
73
80
|
BY_TYPE.fetch(type) do |t|
|
|
74
81
|
raise Error, "[suma] no term classification for type #{t.inspect}"
|
|
75
82
|
end
|
data/lib/suma/term_extractor.rb
CHANGED
|
@@ -7,11 +7,15 @@ require "glossarist"
|
|
|
7
7
|
module Suma
|
|
8
8
|
class TermExtractor
|
|
9
9
|
def initialize(schema_manifest_file, output_path, urn:,
|
|
10
|
-
language_code: "eng"
|
|
10
|
+
language_code: "eng", profile: nil, edition: nil,
|
|
11
|
+
language_codes: nil)
|
|
11
12
|
@schema_manifest_file = File.expand_path(schema_manifest_file)
|
|
12
13
|
@output_path = output_path
|
|
13
14
|
@language_code = language_code
|
|
15
|
+
@language_codes = normalize_languages(language_code, language_codes)
|
|
14
16
|
@urn = Suma::Urn.new(urn)
|
|
17
|
+
@profile = SchemaProfile.fetch(profile)
|
|
18
|
+
@edition = edition
|
|
15
19
|
end
|
|
16
20
|
|
|
17
21
|
def call
|
|
@@ -23,6 +27,12 @@ module Suma
|
|
|
23
27
|
|
|
24
28
|
private
|
|
25
29
|
|
|
30
|
+
# The primary language is always first; additional languages produce
|
|
31
|
+
# linked localizations of the same managed concept.
|
|
32
|
+
def normalize_languages(primary, extra)
|
|
33
|
+
([primary] + Array(extra).map(&:to_s)).map(&:to_s).uniq
|
|
34
|
+
end
|
|
35
|
+
|
|
26
36
|
def validate_inputs
|
|
27
37
|
unless File.exist?(@schema_manifest_file)
|
|
28
38
|
raise Errno::ENOENT, "Specified SCHEMA_MANIFEST_FILE " \
|
|
@@ -56,7 +66,8 @@ module Suma
|
|
|
56
66
|
raise Error, "Schema must have an associated file" unless schema.file
|
|
57
67
|
|
|
58
68
|
classification = TermClassification.for_schema(id: schema.id,
|
|
59
|
-
path: schema.file
|
|
69
|
+
path: schema.file,
|
|
70
|
+
profile: @profile)
|
|
60
71
|
collection = build_managed_concept_collection(schema, classification)
|
|
61
72
|
output_data(collection)
|
|
62
73
|
collection
|
|
@@ -80,30 +91,12 @@ module Suma
|
|
|
80
91
|
end
|
|
81
92
|
|
|
82
93
|
def build_managed_concept_collection(schema, classification)
|
|
83
|
-
source_ref = get_source_ref(schema)
|
|
84
94
|
section_ref = get_section_ref(schema)
|
|
85
95
|
|
|
86
96
|
Glossarist::ManagedConceptCollection.new.tap do |collection|
|
|
87
97
|
schema.entities.each do |entity|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
"#{schema.id}.#{entity.id}-#{@language_code}",
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
localized_concept = build_localized_concept(
|
|
94
|
-
schema: schema,
|
|
95
|
-
entity: entity,
|
|
96
|
-
source_ref: source_ref,
|
|
97
|
-
uuid: localized_concept_id,
|
|
98
|
-
classification: classification,
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
managed_data = Glossarist::V3::ManagedConceptData.new.tap do |data|
|
|
102
|
-
data.id = "#{schema.id}.#{entity.id}"
|
|
103
|
-
data.localizations.store(@language_code, localized_concept)
|
|
104
|
-
data.localized_concepts = { @language_code => localized_concept_id }
|
|
105
|
-
data.domains = [section_ref] if section_ref
|
|
106
|
-
end
|
|
98
|
+
managed_data = build_managed_data(schema, entity, section_ref,
|
|
99
|
+
classification)
|
|
107
100
|
|
|
108
101
|
managed_concept = Glossarist::V3::ManagedConcept.new.tap do |concept|
|
|
109
102
|
concept.id = managed_data.id
|
|
@@ -120,16 +113,45 @@ module Suma
|
|
|
120
113
|
end
|
|
121
114
|
end
|
|
122
115
|
|
|
123
|
-
def
|
|
116
|
+
def build_managed_data(schema, entity, section_ref, classification)
|
|
117
|
+
Glossarist::V3::ManagedConceptData.new.tap do |data|
|
|
118
|
+
data.id = "#{schema.id}.#{entity.id}"
|
|
119
|
+
data.localized_concepts = {}
|
|
120
|
+
|
|
121
|
+
@language_codes.each do |language|
|
|
122
|
+
localized_id = localized_concept_id(schema, entity, language)
|
|
123
|
+
localized = build_localized_concept(
|
|
124
|
+
schema: schema,
|
|
125
|
+
entity: entity,
|
|
126
|
+
language: language,
|
|
127
|
+
uuid: localized_id,
|
|
128
|
+
classification: classification,
|
|
129
|
+
)
|
|
130
|
+
data.localizations.store(language, localized)
|
|
131
|
+
data.localized_concepts[language] = localized_id
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
data.domains = [section_ref] if section_ref
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def localized_concept_id(schema, entity, language)
|
|
139
|
+
Glossarist::Utilities::UUID.uuid_v5(
|
|
140
|
+
Glossarist::Utilities::UUID::OID_NAMESPACE,
|
|
141
|
+
"#{schema.id}.#{entity.id}-#{language}",
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def build_localized_concept(schema:, entity:, language:, uuid:,
|
|
124
146
|
classification:)
|
|
125
147
|
schema_domain = classification.domain_for(schema.id)
|
|
126
148
|
|
|
127
149
|
localized_concept_data = Glossarist::V3::ConceptData.new.tap do |data|
|
|
128
150
|
data.terms = get_entity_terms(entity)
|
|
129
151
|
data.definition = get_entity_definitions(entity, schema, classification)
|
|
130
|
-
data.language_code =
|
|
152
|
+
data.language_code = language
|
|
131
153
|
data.domain = schema_domain
|
|
132
|
-
data.sources = [
|
|
154
|
+
data.sources = [build_source_ref(schema, entity)]
|
|
133
155
|
|
|
134
156
|
notes = NoteProcessor.call(
|
|
135
157
|
entity.remarks,
|
|
@@ -175,26 +197,45 @@ module Suma
|
|
|
175
197
|
)
|
|
176
198
|
end
|
|
177
199
|
|
|
178
|
-
|
|
200
|
+
# Provenance: the schema is the authoritative source, the entity URN is
|
|
201
|
+
# what the concept is about, and the edition/version identifies the
|
|
202
|
+
# owning release.
|
|
203
|
+
def build_source_ref(schema, entity)
|
|
179
204
|
ref = Glossarist::Citation::Ref.new
|
|
180
205
|
ref.source = schema_urn(schema)
|
|
181
|
-
|
|
182
|
-
build_custom_locality(schema).each do |cl|
|
|
183
|
-
case cl.name
|
|
184
|
-
when "version" then ref.version = cl.value
|
|
185
|
-
end
|
|
186
|
-
end
|
|
206
|
+
ref.version = edition_label(schema)
|
|
187
207
|
|
|
188
208
|
origin = Glossarist::V3::Citation.new
|
|
189
209
|
origin.ref = ref
|
|
190
|
-
Glossarist::V3::ConceptSource.new(
|
|
191
|
-
|
|
210
|
+
Glossarist::V3::ConceptSource.new(
|
|
211
|
+
id: schema.id,
|
|
212
|
+
type: "authoritative",
|
|
213
|
+
origin: origin,
|
|
214
|
+
sourced_from: [entity_citation(schema, entity)],
|
|
215
|
+
)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def entity_citation(schema, entity)
|
|
219
|
+
ref = Glossarist::Citation::Ref.new
|
|
220
|
+
ref.source = express_entity_urn("#{schema.id}.#{entity.id}")
|
|
221
|
+
ref.version = edition_label(schema)
|
|
222
|
+
|
|
223
|
+
Glossarist::V3::Citation.new.tap { |citation| citation.ref = ref }
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def edition_label(schema)
|
|
227
|
+
return @edition.to_s if @edition
|
|
228
|
+
|
|
229
|
+
build_custom_locality(schema)
|
|
230
|
+
.find { |locality| locality.name == "version" }&.value
|
|
192
231
|
end
|
|
193
232
|
|
|
194
233
|
def build_custom_locality(schema)
|
|
195
234
|
localities = []
|
|
196
235
|
|
|
197
|
-
version_item = schema.version
|
|
236
|
+
version_item = Array(schema.version&.items).detect do |item|
|
|
237
|
+
item.name == "version"
|
|
238
|
+
end
|
|
198
239
|
if version_item
|
|
199
240
|
localities << Glossarist::CustomLocality.new(name: "version",
|
|
200
241
|
value: version_item.value)
|