lutaml-lml 0.1.7 → 0.2.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/lib/lutaml/lml/cli.rb +12 -3
- data/lib/lutaml/lml/data_processor/attribute_processing.rb +49 -9
- data/lib/lutaml/lml/data_processor/instance_processing.rb +5 -0
- data/lib/lutaml/lml/data_processor/value_declaration_processing.rb +62 -0
- data/lib/lutaml/lml/data_processor.rb +3 -0
- data/lib/lutaml/lml/document_builder.rb +235 -9
- data/lib/lutaml/lml/grammar/lml.artifact.json +6473 -2425
- data/lib/lutaml/lml/grammar/lml.parg +82 -19
- data/lib/lutaml/lml/model_compiler.rb +392 -39
- data/lib/lutaml/lml/models/data_type.rb +3 -0
- data/lib/lutaml/lml/models/derived_field.rb +14 -0
- data/lib/lutaml/lml/models/document.rb +2 -0
- data/lib/lutaml/lml/models/enum.rb +3 -0
- data/lib/lutaml/lml/models/instance.rb +2 -0
- data/lib/lutaml/lml/models/instance_collection.rb +1 -0
- data/lib/lutaml/lml/models/mapping.rb +15 -0
- data/lib/lutaml/lml/models/mapping_rule.rb +16 -0
- data/lib/lutaml/lml/models/member_field.rb +12 -0
- data/lib/lutaml/lml/models/namespace.rb +14 -0
- data/lib/lutaml/lml/models/package.rb +1 -0
- data/lib/lutaml/lml/models/serialization_mapping.rb +17 -0
- data/lib/lutaml/lml/models/string_format.rb +16 -0
- data/lib/lutaml/lml/models/top_element_attribute.rb +4 -0
- data/lib/lutaml/lml/models/uml_class.rb +4 -0
- data/lib/lutaml/lml/preprocessor.rb +82 -6
- data/lib/lutaml/lml/validator.rb +276 -0
- data/lib/lutaml/lml/version.rb +1 -1
- data/lib/lutaml/lml.rb +8 -0
- metadata +11 -2
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Lml
|
|
5
|
+
# A lutaml-model wire-name mapping declared inside an LML class:
|
|
6
|
+
# mapping yaml { map "full-name", to: "name" }
|
|
7
|
+
# One entry per map line; `format` is the target serialization
|
|
8
|
+
# (yaml, xml, json, toml, csv).
|
|
9
|
+
class Mapping < Lutaml::Model::Serializable
|
|
10
|
+
attribute :format, :string
|
|
11
|
+
attribute :wire, :string
|
|
12
|
+
attribute :to, :string
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Lml
|
|
5
|
+
# One line of a serialization mapping: what kind of mapping (element,
|
|
6
|
+
# attribute, content, map, key, value), the serialization-side wire
|
|
7
|
+
# name, the model attribute it binds, and the qualifier mode for
|
|
8
|
+
# key/value collections (to_instance, as_attribute).
|
|
9
|
+
class MappingRule < Lutaml::Model::Serializable
|
|
10
|
+
attribute :kind, :string
|
|
11
|
+
attribute :wire, :string
|
|
12
|
+
attribute :field, :string
|
|
13
|
+
attribute :mode, :string
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Lml
|
|
5
|
+
# A typed payload field declared on an enum:
|
|
6
|
+
# member_fields { abbreviation String urn_segment String }
|
|
7
|
+
class MemberField < Lutaml::Model::Serializable
|
|
8
|
+
attribute :name, :string
|
|
9
|
+
attribute :type, :string
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Lml
|
|
5
|
+
# RS 3010 extension: a declared XML namespace (uri + optional prefix).
|
|
6
|
+
# The compiler generates the corresponding Lutaml::Model::XmlNamespace
|
|
7
|
+
# subclass for the compiled model namespace.
|
|
8
|
+
class Namespace < Lutaml::Model::Serializable
|
|
9
|
+
attribute :name, :string
|
|
10
|
+
attribute :uri, :string
|
|
11
|
+
attribute :prefix, :string
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -17,6 +17,7 @@ module Lutaml
|
|
|
17
17
|
attribute :data_types, "Lutaml::Lml::DataType", collection: true, default: -> { [] }
|
|
18
18
|
attribute :packages, "Lutaml::Lml::Package", collection: true, default: -> { [] }
|
|
19
19
|
attribute :diagrams, "Lutaml::Lml::Diagram", collection: true, default: -> { [] }
|
|
20
|
+
attribute :default_string_formats, "Lutaml::Lml::StringFormat", collection: true, default: -> { [] }
|
|
20
21
|
end
|
|
21
22
|
end
|
|
22
23
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Lml
|
|
5
|
+
# RS 3010 extension: a serialization mapping declared separately from
|
|
6
|
+
# its class and linked by target name —
|
|
7
|
+
# mapping xml Ceramic { element "ceramic" … }
|
|
8
|
+
class SerializationMapping < Lutaml::Model::Serializable
|
|
9
|
+
attribute :format, :string
|
|
10
|
+
attribute :target, :string
|
|
11
|
+
attribute :element_name, :string
|
|
12
|
+
attribute :namespace_ref, :string
|
|
13
|
+
attribute :mixed_content, :boolean, default: false
|
|
14
|
+
attribute :rules, 'Lutaml::Lml::MappingRule', collection: true, default: -> { [] }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Lml
|
|
5
|
+
# A grammar-backed string format declared on a class (or as a
|
|
6
|
+
# models-block default): a thin reference — the artifact owns the
|
|
7
|
+
# capture maps and render specs; LML only names format, artifact,
|
|
8
|
+
# and entry root.
|
|
9
|
+
# string_format :pubid { artifact "pubid/iso" root "iso_identifier" }
|
|
10
|
+
class StringFormat < Lutaml::Model::Serializable
|
|
11
|
+
attribute :format, :string
|
|
12
|
+
attribute :artifact, :string
|
|
13
|
+
attribute :root, :string
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -19,6 +19,8 @@ module Lutaml
|
|
|
19
19
|
attribute :definition, Lutaml::Lml::Types::TextType
|
|
20
20
|
attribute :association, :string
|
|
21
21
|
attribute :default, :string
|
|
22
|
+
attribute :value_set, :string, collection: true, default: -> { [] }
|
|
23
|
+
attribute :pattern, :string
|
|
22
24
|
|
|
23
25
|
# LML-specific attributes
|
|
24
26
|
attribute :properties, "Lutaml::Lml::TopElementAttribute", collection: true, default: -> { [] }
|
|
@@ -53,6 +55,8 @@ module Lutaml
|
|
|
53
55
|
map "definition", to: :definition
|
|
54
56
|
map "association", to: :association
|
|
55
57
|
map "default", to: :default
|
|
58
|
+
map "value_set", to: :value_set
|
|
59
|
+
map "pattern", to: :pattern
|
|
56
60
|
map "properties", to: :properties
|
|
57
61
|
map "value", to: :value, with: { to: :value_to, from: :value_from }
|
|
58
62
|
map "attributes", to: :attributes
|
|
@@ -23,6 +23,10 @@ module Lutaml
|
|
|
23
23
|
attribute :operations, "Lutaml::Lml::Operation", collection: true
|
|
24
24
|
attribute :data_types, "Lutaml::Lml::DataType", collection: true
|
|
25
25
|
attribute :associations, "Lutaml::Lml::Association", collection: true
|
|
26
|
+
attribute :wire_mappings, "Lutaml::Lml::Mapping", collection: true, default: -> { [] }
|
|
27
|
+
attribute :string_formats, "Lutaml::Lml::StringFormat", collection: true, default: -> { [] }
|
|
28
|
+
attribute :derived_fields, "Lutaml::Lml::DerivedField", collection: true, default: -> { [] }
|
|
29
|
+
attribute :serialization_mappings, "Lutaml::Lml::SerializationMapping", collection: true, default: -> { [] }
|
|
26
30
|
|
|
27
31
|
# LML-specific
|
|
28
32
|
attribute :parent_class, :string
|
|
@@ -21,16 +21,19 @@ module Lutaml
|
|
|
21
21
|
|
|
22
22
|
private
|
|
23
23
|
|
|
24
|
+
# RS 3001 §Comment: `//` to end of line and `/* ... */` block
|
|
25
|
+
# comments are stripped quote-aware before include expansion, so
|
|
26
|
+
# string literals (e.g. URLs) survive intact.
|
|
24
27
|
def expand_lines(text, base_dir, chain)
|
|
28
|
+
text = strip_comments(text)
|
|
25
29
|
text.split(/\r?\n/)
|
|
26
30
|
.flat_map { |line| expand_line(line, base_dir, chain) }
|
|
27
31
|
.join("\n")
|
|
28
32
|
end
|
|
29
33
|
|
|
30
34
|
def expand_line(line, base_dir, chain)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return [cleaned] unless path
|
|
35
|
+
path = include_path(line, base_dir)
|
|
36
|
+
return [line] unless path
|
|
34
37
|
|
|
35
38
|
raise Error, "circular include detected: #{path}" if chain.include?(path)
|
|
36
39
|
|
|
@@ -45,13 +48,86 @@ module Lutaml
|
|
|
45
48
|
end
|
|
46
49
|
|
|
47
50
|
def read_included(path)
|
|
48
|
-
File.read(path)
|
|
51
|
+
File.read(path, encoding: "UTF-8")
|
|
49
52
|
rescue Errno::ENOENT, Errno::EACCES => e
|
|
50
53
|
raise Error, "cannot read include #{path}: #{e.message}"
|
|
51
54
|
end
|
|
52
55
|
|
|
53
|
-
def
|
|
54
|
-
|
|
56
|
+
def strip_comments(text)
|
|
57
|
+
scanner = CommentScanner.new(text)
|
|
58
|
+
out = +""
|
|
59
|
+
until scanner.done?
|
|
60
|
+
scanner.emit(out)
|
|
61
|
+
end
|
|
62
|
+
out
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Quote-aware scanner removing `//` and `/* */` comments while
|
|
66
|
+
# preserving string literals and line structure.
|
|
67
|
+
class CommentScanner
|
|
68
|
+
def initialize(text)
|
|
69
|
+
@text = text
|
|
70
|
+
@pos = 0
|
|
71
|
+
@quote = nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def done?
|
|
75
|
+
@pos >= @text.length
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def emit(out)
|
|
79
|
+
return emit_quoted(out) if @quote
|
|
80
|
+
return emit_delimiter(out) if string_delimiter?(@text[@pos])
|
|
81
|
+
return emit_block_comment(out) if block_comment_start?
|
|
82
|
+
return skip_line_comment if line_comment_start?
|
|
83
|
+
|
|
84
|
+
out << @text[@pos]
|
|
85
|
+
@pos += 1
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def string_delimiter?(ch)
|
|
91
|
+
ch == '"' || ch == "'"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def emit_delimiter(out)
|
|
95
|
+
@quote = @text[@pos]
|
|
96
|
+
out << @text[@pos]
|
|
97
|
+
@pos += 1
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def emit_quoted(out)
|
|
101
|
+
ch = @text[@pos]
|
|
102
|
+
out << ch
|
|
103
|
+
if ch == "\\"
|
|
104
|
+
out << @text[@pos + 1].to_s
|
|
105
|
+
@pos += 2
|
|
106
|
+
else
|
|
107
|
+
@quote = nil if ch == @quote
|
|
108
|
+
@pos += 1
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def block_comment_start?
|
|
113
|
+
@text[@pos, 2] == "/*"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def emit_block_comment(out)
|
|
117
|
+
close = @text.index("*/", @pos + 2)
|
|
118
|
+
raise Lutaml::Lml::Error, "unterminated block comment" unless close
|
|
119
|
+
|
|
120
|
+
out << "\n" * @text[@pos...close].count("\n") << " "
|
|
121
|
+
@pos = close + 2
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def line_comment_start?
|
|
125
|
+
@text[@pos, 2] == "//"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def skip_line_comment
|
|
129
|
+
@pos = @text.index("\n", @pos) || @text.length
|
|
130
|
+
end
|
|
55
131
|
end
|
|
56
132
|
end
|
|
57
133
|
end
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Lml
|
|
5
|
+
# RS 3001 §Validation rules evaluated over a parsed document:
|
|
6
|
+
# class constraints (unique names, unique attribute names), attribute
|
|
7
|
+
# constraints (mandatory cardinality-1 attributes populated, values
|
|
8
|
+
# matching declared types) and reference validity (ref: paths resolve
|
|
9
|
+
# to instances and are acyclic).
|
|
10
|
+
class Validator
|
|
11
|
+
Violation = Struct.new(:rule, :message, keyword_init: true)
|
|
12
|
+
|
|
13
|
+
PRIMITIVE_CONFORMITY = {
|
|
14
|
+
'Integer' => /\A-?\d+\z/,
|
|
15
|
+
'Number' => /\A-?\d+\z/,
|
|
16
|
+
'Float' => /\A-?\d+\.\d+\z/,
|
|
17
|
+
'Boolean' => /\A(true|false)\z/
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
COLLECTION_KEYS = %i[properties attributes instances].freeze
|
|
21
|
+
|
|
22
|
+
def self.violations(document)
|
|
23
|
+
new(document).violations
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(document)
|
|
27
|
+
@document = document
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def violations
|
|
31
|
+
unique_name_violations +
|
|
32
|
+
unique_attribute_violations +
|
|
33
|
+
instance_violations +
|
|
34
|
+
circular_reference_violations
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def all_definitions
|
|
40
|
+
@all_definitions ||= begin
|
|
41
|
+
pairs = []
|
|
42
|
+
Array(@document.classes).each { |c| pairs << [c.name.to_s, c] }
|
|
43
|
+
Array(@document.enums).each { |e| pairs << [e.name.to_s, e] }
|
|
44
|
+
Array(@document.data_types).each { |d| pairs << [d.name.to_s, d] }
|
|
45
|
+
Array(@document.packages).each { |pkg| collect_package_definitions(pkg, pairs) }
|
|
46
|
+
pairs
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def collect_package_definitions(pkg, pairs)
|
|
51
|
+
Array(pkg.classes).each { |c| pairs << [c.name.to_s, c] }
|
|
52
|
+
Array(pkg.enums).each { |e| pairs << [e.name.to_s, e] }
|
|
53
|
+
Array(pkg.data_types).each { |d| pairs << [d.name.to_s, d] }
|
|
54
|
+
Array(pkg.packages).each { |nested| collect_package_definitions(nested, pairs) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def type_definitions
|
|
58
|
+
@type_definitions ||= all_definitions.to_h
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def unique_name_violations
|
|
62
|
+
names = all_definitions.map(&:first)
|
|
63
|
+
names.tally.filter_map do |name, count|
|
|
64
|
+
next if count == 1
|
|
65
|
+
|
|
66
|
+
Violation.new(
|
|
67
|
+
rule: 'unique_names',
|
|
68
|
+
message: "'#{name}' is defined #{count} times"
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def unique_attribute_violations
|
|
74
|
+
type_definitions.filter_map do |name, definition|
|
|
75
|
+
dupes = attribute_names(definition)
|
|
76
|
+
.tally.select { |_attr, count| count > 1 }.keys
|
|
77
|
+
next if dupes.empty?
|
|
78
|
+
|
|
79
|
+
Violation.new(
|
|
80
|
+
rule: 'unique_attribute_names',
|
|
81
|
+
message: "#{name}: duplicate attribute(s) #{dupes.inspect}"
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def attribute_names(definition)
|
|
87
|
+
Array(definition.attributes).map(&:name).map(&:to_s)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def instance_violations
|
|
91
|
+
instances.flat_map do |instance|
|
|
92
|
+
mandatory_violations(instance) +
|
|
93
|
+
type_conformity_violations(instance) +
|
|
94
|
+
reference_violations(instance)
|
|
95
|
+
end.compact
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def instances
|
|
99
|
+
Array(@document.instances&.instances)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def mandatory_violations(instance)
|
|
103
|
+
definition = type_definitions[definition_type(instance)]
|
|
104
|
+
return [] unless definition.is_a?(UmlClass) || definition.is_a?(DataType)
|
|
105
|
+
|
|
106
|
+
definition.attributes.filter_map do |attr|
|
|
107
|
+
next unless mandatory?(attr)
|
|
108
|
+
next if populated?(instance, attr.name.to_s)
|
|
109
|
+
|
|
110
|
+
Violation.new(
|
|
111
|
+
rule: 'mandatory_attributes',
|
|
112
|
+
message: "instance '#{instance_name(instance)}' (#{definition_type(instance)}): " \
|
|
113
|
+
"mandatory attribute '#{attr.name}' is not populated"
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def mandatory?(attr)
|
|
119
|
+
attr.cardinality && attr.cardinality.min == '1'
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def populated?(instance, attr_name)
|
|
123
|
+
pair = instance_attribute(instance, attr_name)
|
|
124
|
+
return false unless pair
|
|
125
|
+
|
|
126
|
+
value = pair[1]
|
|
127
|
+
value.respond_to?(:value) ? !value.to_s.empty? : !value.to_s.empty?
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def type_conformity_violations(instance)
|
|
131
|
+
definition = type_definitions[definition_type(instance)]
|
|
132
|
+
return [] unless definition.respond_to?(:attributes)
|
|
133
|
+
|
|
134
|
+
instance_attribute_pairs(instance).filter_map do |name, value|
|
|
135
|
+
attr = Array(definition.attributes).find { |a| a.name.to_s == name }
|
|
136
|
+
next unless attr
|
|
137
|
+
|
|
138
|
+
conformity_violation(instance, name, attr, value)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def conformity_violation(instance, name, attr, value)
|
|
143
|
+
pattern = PRIMITIVE_CONFORMITY[attr.type.to_s]
|
|
144
|
+
return unless pattern
|
|
145
|
+
return if pattern.match?(value.to_s)
|
|
146
|
+
|
|
147
|
+
Violation.new(
|
|
148
|
+
rule: 'type_conformity',
|
|
149
|
+
message: "instance '#{instance_name(instance)}': attribute '#{name}' " \
|
|
150
|
+
"expects #{attr.type}, got #{value.to_s.inspect}"
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def reference_violations(instance)
|
|
155
|
+
reference_values(instance).filter_map do |name, path|
|
|
156
|
+
segments = path.to_s.split('.')
|
|
157
|
+
unless resolvable_reference?(segments)
|
|
158
|
+
next Violation.new(
|
|
159
|
+
rule: 'reference_validity',
|
|
160
|
+
message: "instance '#{instance_name(instance)}': attribute '#{name}' " \
|
|
161
|
+
"references unknown path '#{path}'"
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
attribute_violation(instance, name, path, segments.first)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# RS 3001 reference paths: `instance` / `instance.attribute` /
|
|
170
|
+
# `collection.instance`.
|
|
171
|
+
def resolvable_reference?(segments)
|
|
172
|
+
case segments.length
|
|
173
|
+
when 1 then instance_names.include?(segments.first)
|
|
174
|
+
when 2 then instance_names.include?(segments.first) ||
|
|
175
|
+
collection_names.include?(segments.first)
|
|
176
|
+
else false
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def collection_names
|
|
181
|
+
@collection_names ||= [@document.instances&.name].compact
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def attribute_violation(instance, _name, path, target)
|
|
185
|
+
segment = path.to_s.split('.')[1]
|
|
186
|
+
target_instance = instances.find { |i| instance_name(i) == target }
|
|
187
|
+
return if segment.nil? || target_instance.nil? ||
|
|
188
|
+
instance_attribute(target_instance, segment)
|
|
189
|
+
|
|
190
|
+
Violation.new(
|
|
191
|
+
rule: 'reference_validity',
|
|
192
|
+
message: "instance '#{instance_name(instance)}': reference '#{path}' " \
|
|
193
|
+
"does not resolve (no attribute '#{segment}' on '#{target}')"
|
|
194
|
+
)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def instance_attribute_pairs(instance)
|
|
198
|
+
pairs = []
|
|
199
|
+
instance.each_attribute do |name, value, instances|
|
|
200
|
+
pairs << [name.to_s, value_for(value, instances)]
|
|
201
|
+
end
|
|
202
|
+
pairs
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def instance_attribute(instance, attr_name)
|
|
206
|
+
instance_attribute_pairs(instance).find { |name, _value| name == attr_name }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def value_for(value, instances)
|
|
210
|
+
literal = value.respond_to?(:value) ? value.value : value
|
|
211
|
+
return instances.first if literal.is_a?(Hash) && literal[:instances]
|
|
212
|
+
|
|
213
|
+
literal
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def reference_values(instance)
|
|
217
|
+
instance_attribute_pairs(instance).filter_map do |name, value|
|
|
218
|
+
literal = value.respond_to?(:value) ? value.value : value
|
|
219
|
+
next unless literal.is_a?(Hash) && literal[:reference]
|
|
220
|
+
|
|
221
|
+
[name, literal[:reference]]
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def instance_names
|
|
226
|
+
@instance_names ||= instances.map { |i| instance_name(i) }
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# RS 3001: circular references are invalid.
|
|
230
|
+
def circular_reference_violations
|
|
231
|
+
graph = instances.to_h do |instance|
|
|
232
|
+
targets = reference_values(instance)
|
|
233
|
+
.map { |_name, path| path.to_s.split('.').first }
|
|
234
|
+
[instance_name(instance), targets]
|
|
235
|
+
end
|
|
236
|
+
cycles = cycles_in(graph)
|
|
237
|
+
cycles.map do |cycle|
|
|
238
|
+
Violation.new(
|
|
239
|
+
rule: 'reference_circularity',
|
|
240
|
+
message: "circular reference: #{cycle.join(' -> ')}"
|
|
241
|
+
)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def cycles_in(graph)
|
|
246
|
+
cycles = []
|
|
247
|
+
graph.each_key do |start|
|
|
248
|
+
path = walk_for_cycle(graph, start)
|
|
249
|
+
cycles << path if path
|
|
250
|
+
end
|
|
251
|
+
cycles.uniq { |cycle| cycle.sort }
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def walk_for_cycle(graph, start)
|
|
255
|
+
path = [start]
|
|
256
|
+
node = start
|
|
257
|
+
loop do
|
|
258
|
+
nexts = graph[node] || []
|
|
259
|
+
node = nexts.first
|
|
260
|
+
return nil if node.nil? || node == '' || !graph.key?(node)
|
|
261
|
+
return path + [node] if path.include?(node)
|
|
262
|
+
|
|
263
|
+
path << node
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def instance_name(instance)
|
|
268
|
+
instance.respond_to?(:name) ? instance.name.to_s : ''
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def definition_type(instance)
|
|
272
|
+
(instance.isa || instance.type).to_s
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
data/lib/lutaml/lml/version.rb
CHANGED
data/lib/lutaml/lml.rb
CHANGED
|
@@ -44,6 +44,7 @@ module Lutaml
|
|
|
44
44
|
autoload :ModelCompiler, "lutaml/lml/model_compiler"
|
|
45
45
|
autoload :ImportResolver, "lutaml/lml/import_resolver"
|
|
46
46
|
autoload :ViewResolver, "lutaml/lml/view_resolver"
|
|
47
|
+
autoload :Validator, "lutaml/lml/validator"
|
|
47
48
|
autoload :ViewResolution, "lutaml/lml/view_resolution"
|
|
48
49
|
autoload :EntityTypes, "lutaml/lml/entity_types"
|
|
49
50
|
autoload :AssociationLabelResolver, "lutaml/lml/association_label_resolver"
|
|
@@ -71,6 +72,10 @@ module Lutaml
|
|
|
71
72
|
autoload :InstanceCollection, "lutaml/lml/models/instance_collection"
|
|
72
73
|
autoload :InstancesExport, "lutaml/lml/models/instances_export"
|
|
73
74
|
autoload :InstancesImport, "lutaml/lml/models/instances_import"
|
|
75
|
+
autoload :DerivedField, "lutaml/lml/models/derived_field"
|
|
76
|
+
autoload :Mapping, "lutaml/lml/models/mapping"
|
|
77
|
+
autoload :MemberField, "lutaml/lml/models/member_field"
|
|
78
|
+
autoload :StringFormat, "lutaml/lml/models/string_format"
|
|
74
79
|
autoload :Operation, "lutaml/lml/models/operation"
|
|
75
80
|
autoload :OperationParameter, "lutaml/lml/models/operation_parameter"
|
|
76
81
|
autoload :Package, "lutaml/lml/models/package"
|
|
@@ -80,6 +85,9 @@ module Lutaml
|
|
|
80
85
|
autoload :Value, "lutaml/lml/models/value"
|
|
81
86
|
autoload :ViewFilter, "lutaml/lml/models/view_filter"
|
|
82
87
|
autoload :ViewImport, "lutaml/lml/models/view_import"
|
|
88
|
+
autoload :MappingRule, "lutaml/lml/models/mapping_rule"
|
|
89
|
+
autoload :Namespace, "lutaml/lml/models/namespace"
|
|
90
|
+
autoload :SerializationMapping, "lutaml/lml/models/serialization_mapping"
|
|
83
91
|
|
|
84
92
|
# Namespaces with their own autoloads
|
|
85
93
|
autoload :Grammar, "lutaml/lml/grammar"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lutaml-lml
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: lutaml-model
|
|
@@ -84,6 +84,7 @@ files:
|
|
|
84
84
|
- lib/lutaml/lml/data_processor/attribute_processing.rb
|
|
85
85
|
- lib/lutaml/lml/data_processor/collection_processing.rb
|
|
86
86
|
- lib/lutaml/lml/data_processor/instance_processing.rb
|
|
87
|
+
- lib/lutaml/lml/data_processor/value_declaration_processing.rb
|
|
87
88
|
- lib/lutaml/lml/data_processor/value_processing.rb
|
|
88
89
|
- lib/lutaml/lml/data_processor/view_processing.rb
|
|
89
90
|
- lib/lutaml/lml/document_builder.rb
|
|
@@ -122,6 +123,7 @@ files:
|
|
|
122
123
|
- lib/lutaml/lml/models/collection.rb
|
|
123
124
|
- lib/lutaml/lml/models/constraint.rb
|
|
124
125
|
- lib/lutaml/lml/models/data_type.rb
|
|
126
|
+
- lib/lutaml/lml/models/derived_field.rb
|
|
125
127
|
- lib/lutaml/lml/models/diagram.rb
|
|
126
128
|
- lib/lutaml/lml/models/document.rb
|
|
127
129
|
- lib/lutaml/lml/models/enum.rb
|
|
@@ -131,10 +133,16 @@ files:
|
|
|
131
133
|
- lib/lutaml/lml/models/instance_collection.rb
|
|
132
134
|
- lib/lutaml/lml/models/instances_export.rb
|
|
133
135
|
- lib/lutaml/lml/models/instances_import.rb
|
|
136
|
+
- lib/lutaml/lml/models/mapping.rb
|
|
137
|
+
- lib/lutaml/lml/models/mapping_rule.rb
|
|
138
|
+
- lib/lutaml/lml/models/member_field.rb
|
|
139
|
+
- lib/lutaml/lml/models/namespace.rb
|
|
134
140
|
- lib/lutaml/lml/models/operation.rb
|
|
135
141
|
- lib/lutaml/lml/models/operation_parameter.rb
|
|
136
142
|
- lib/lutaml/lml/models/package.rb
|
|
137
143
|
- lib/lutaml/lml/models/primitive_type.rb
|
|
144
|
+
- lib/lutaml/lml/models/serialization_mapping.rb
|
|
145
|
+
- lib/lutaml/lml/models/string_format.rb
|
|
138
146
|
- lib/lutaml/lml/models/top_element_attribute.rb
|
|
139
147
|
- lib/lutaml/lml/models/uml_class.rb
|
|
140
148
|
- lib/lutaml/lml/models/value.rb
|
|
@@ -147,6 +155,7 @@ files:
|
|
|
147
155
|
- lib/lutaml/lml/transform.rb
|
|
148
156
|
- lib/lutaml/lml/types.rb
|
|
149
157
|
- lib/lutaml/lml/types/text_type.rb
|
|
158
|
+
- lib/lutaml/lml/validator.rb
|
|
150
159
|
- lib/lutaml/lml/version.rb
|
|
151
160
|
- lib/lutaml/lml/view_resolution.rb
|
|
152
161
|
- lib/lutaml/lml/view_resolver.rb
|