lutaml-lml 0.1.2 → 0.1.4
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 +1 -1
- data/lib/lutaml/lml/data_processor/attribute_processing.rb +1 -0
- data/lib/lutaml/lml/data_processor/instance_processing.rb +11 -2
- data/lib/lutaml/lml/data_processor/view_processing.rb +7 -6
- data/lib/lutaml/lml/document_builder.rb +1 -8
- data/lib/lutaml/lml/entity_types.rb +30 -0
- data/lib/lutaml/lml/executor/condition_evaluator.rb +1 -1
- data/lib/lutaml/lml/executor.rb +4 -5
- data/lib/lutaml/lml/format/adapter/standard_adapter.rb +1 -1
- data/lib/lutaml/lml/formatter/base.rb +7 -4
- data/lib/lutaml/lml/grammar/concerns/definitions.rb +29 -21
- data/lib/lutaml/lml/grammar/concerns/primitives.rb +11 -2
- data/lib/lutaml/lml/grammar/concerns/view_rules.rb +26 -8
- data/lib/lutaml/lml/grammar/instances.rb +5 -2
- data/lib/lutaml/lml/import_resolver.rb +45 -47
- data/lib/lutaml/lml/literal_value.rb +32 -0
- data/lib/lutaml/lml/model_compiler.rb +3 -4
- data/lib/lutaml/lml/models/association.rb +1 -1
- data/lib/lutaml/lml/models/constraint.rb +1 -1
- data/lib/lutaml/lml/models/data_type.rb +5 -1
- data/lib/lutaml/lml/models/diagram.rb +1 -1
- data/lib/lutaml/lml/models/document.rb +8 -7
- data/lib/lutaml/lml/models/enum.rb +5 -1
- data/lib/lutaml/lml/models/instance_collection.rb +1 -1
- data/lib/lutaml/lml/models/operation.rb +1 -1
- data/lib/lutaml/lml/models/package.rb +1 -1
- data/lib/lutaml/lml/models/primitive_type.rb +5 -1
- data/lib/lutaml/lml/models/top_element_attribute.rb +68 -5
- data/lib/lutaml/lml/models/uml_class.rb +11 -18
- data/lib/lutaml/lml/models/value.rb +1 -1
- data/lib/lutaml/lml/parser.rb +3 -8
- data/lib/lutaml/lml/pipeline.rb +9 -30
- data/lib/lutaml/lml/preprocessor.rb +26 -21
- data/lib/lutaml/lml/source.rb +49 -0
- data/lib/lutaml/lml/transform.rb +5 -1
- data/lib/lutaml/lml/types/text_type.rb +28 -0
- data/lib/lutaml/lml/types.rb +10 -0
- data/lib/lutaml/lml/version.rb +1 -1
- data/lib/lutaml/lml/view_resolution.rb +32 -0
- data/lib/lutaml/lml/view_resolver.rb +5 -5
- data/lib/lutaml/lml.rb +21 -0
- metadata +8 -2
|
@@ -7,10 +7,10 @@ module Lutaml
|
|
|
7
7
|
attribute :name, :string
|
|
8
8
|
attribute :title, :string
|
|
9
9
|
attribute :caption, :string
|
|
10
|
-
attribute :groups, "Lutaml::Lml::Group", collection: true
|
|
10
|
+
attribute :groups, "Lutaml::Lml::Group", collection: true, default: -> { [] }
|
|
11
11
|
attribute :fidelity, "Lutaml::Lml::Fidelity"
|
|
12
12
|
attribute :fontname, :string
|
|
13
|
-
attribute :comments, :string, collection: true
|
|
13
|
+
attribute :comments, :string, collection: true, default: -> { [] }
|
|
14
14
|
|
|
15
15
|
attribute :classes, "Lutaml::Lml::UmlClass", collection: true, default: -> { [] }
|
|
16
16
|
attribute :data_types, "Lutaml::Lml::DataType", collection: true, default: -> { [] }
|
|
@@ -20,11 +20,12 @@ module Lutaml
|
|
|
20
20
|
attribute :associations, "Lutaml::Lml::Association", collection: true, default: -> { [] }
|
|
21
21
|
attribute :diagrams, "Lutaml::Lml::Diagram", collection: true, default: -> { [] }
|
|
22
22
|
|
|
23
|
-
# LML-specific
|
|
23
|
+
# LML-specific. instance/instances/fidelity/show_filter/hide_filter
|
|
24
|
+
# are deliberately default-less: nil means "absent".
|
|
24
25
|
attribute :instance, "Lutaml::Lml::Instance"
|
|
25
|
-
attribute :requires, :string, collection: true
|
|
26
|
+
attribute :requires, :string, collection: true, default: -> { [] }
|
|
26
27
|
attribute :instances, "Lutaml::Lml::InstanceCollection"
|
|
27
|
-
attribute :view_imports, "Lutaml::Lml::ViewImport", collection: true
|
|
28
|
+
attribute :view_imports, "Lutaml::Lml::ViewImport", collection: true, default: -> { [] }
|
|
28
29
|
attribute :show_filter, "Lutaml::Lml::ViewFilter"
|
|
29
30
|
attribute :hide_filter, "Lutaml::Lml::ViewFilter"
|
|
30
31
|
|
|
@@ -32,13 +33,13 @@ module Lutaml
|
|
|
32
33
|
# formatters that need to walk every classifiable type without
|
|
33
34
|
# caring which collection holds it.
|
|
34
35
|
def all_classes
|
|
35
|
-
|
|
36
|
+
EntityTypes.all.flat_map { |type| public_send(type.entity_type) }
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
# Class-like entities that can own associations. Excludes enums
|
|
39
40
|
# (which have no attributes/associations of their own).
|
|
40
41
|
def classifiable_classes
|
|
41
|
-
|
|
42
|
+
EntityTypes.classifiable.flat_map { |type| public_send(type.entity_type) }
|
|
42
43
|
end
|
|
43
44
|
end
|
|
44
45
|
end
|
|
@@ -5,7 +5,7 @@ module Lutaml
|
|
|
5
5
|
class Enum < Lutaml::Model::Serializable
|
|
6
6
|
# From TopElement
|
|
7
7
|
attribute :name, :string
|
|
8
|
-
attribute :definition,
|
|
8
|
+
attribute :definition, Lutaml::Lml::Types::TextType
|
|
9
9
|
attribute :keyword, :string, default: "enumeration"
|
|
10
10
|
attribute :stereotype, :string, collection: true, default: -> { [] }
|
|
11
11
|
attribute :visibility, :string, default: "public"
|
|
@@ -23,6 +23,10 @@ module Lutaml
|
|
|
23
23
|
def self.entity_type
|
|
24
24
|
:enums
|
|
25
25
|
end
|
|
26
|
+
|
|
27
|
+
def self.classifiable?
|
|
28
|
+
false
|
|
29
|
+
end
|
|
26
30
|
end
|
|
27
31
|
end
|
|
28
32
|
end
|
|
@@ -6,7 +6,7 @@ module Lutaml
|
|
|
6
6
|
attribute :instances, "Lutaml::Lml::Instance", collection: true, default: []
|
|
7
7
|
attribute :imports, "Lutaml::Lml::InstancesImport", collection: true, default: []
|
|
8
8
|
attribute :exports, "Lutaml::Lml::InstancesExport", collection: true, default: []
|
|
9
|
-
attribute :collections, "Lutaml::Lml::Collection", default: []
|
|
9
|
+
attribute :collections, "Lutaml::Lml::Collection", collection: true, default: []
|
|
10
10
|
end
|
|
11
11
|
end
|
|
12
12
|
end
|
|
@@ -5,7 +5,7 @@ module Lutaml
|
|
|
5
5
|
class Operation < Lutaml::Model::Serializable
|
|
6
6
|
# From TopElement
|
|
7
7
|
attribute :name, :string
|
|
8
|
-
attribute :definition,
|
|
8
|
+
attribute :definition, Lutaml::Lml::Types::TextType
|
|
9
9
|
attribute :keyword, :string
|
|
10
10
|
attribute :stereotype, :string, collection: true, default: -> { [] }
|
|
11
11
|
attribute :visibility, :string, default: "public"
|
|
@@ -5,7 +5,7 @@ module Lutaml
|
|
|
5
5
|
class Package < Lutaml::Model::Serializable
|
|
6
6
|
# From TopElement
|
|
7
7
|
attribute :name, :string
|
|
8
|
-
attribute :definition,
|
|
8
|
+
attribute :definition, Lutaml::Lml::Types::TextType
|
|
9
9
|
attribute :keyword, :string
|
|
10
10
|
attribute :stereotype, :string, collection: true, default: -> { [] }
|
|
11
11
|
attribute :visibility, :string, default: "public"
|
|
@@ -7,7 +7,7 @@ module Lutaml
|
|
|
7
7
|
# accesses the same attributes as DataType via format_class.
|
|
8
8
|
attribute :name, :string
|
|
9
9
|
attribute :keyword, :string, default: "primitive"
|
|
10
|
-
attribute :definition,
|
|
10
|
+
attribute :definition, Lutaml::Lml::Types::TextType
|
|
11
11
|
attribute :stereotype, :string, collection: true, default: -> { [] }
|
|
12
12
|
attribute :visibility, :string, default: "public"
|
|
13
13
|
attribute :is_abstract, :boolean, default: false
|
|
@@ -21,6 +21,10 @@ module Lutaml
|
|
|
21
21
|
def self.entity_type
|
|
22
22
|
:primitives
|
|
23
23
|
end
|
|
24
|
+
|
|
25
|
+
def self.classifiable?
|
|
26
|
+
true
|
|
27
|
+
end
|
|
24
28
|
end
|
|
25
29
|
end
|
|
26
30
|
end
|
|
@@ -16,16 +16,79 @@ module Lutaml
|
|
|
16
16
|
attribute :is_static, :boolean, default: false
|
|
17
17
|
attribute :is_read_only, :boolean, default: false
|
|
18
18
|
attribute :stereotype, :string, collection: true, default: -> { [] }
|
|
19
|
-
attribute :definition,
|
|
19
|
+
attribute :definition, Lutaml::Lml::Types::TextType
|
|
20
20
|
attribute :association, :string
|
|
21
21
|
attribute :default, :string
|
|
22
22
|
|
|
23
23
|
# LML-specific attributes
|
|
24
|
-
attribute :properties, "Lutaml::Lml::TopElementAttribute", collection: true, default: []
|
|
25
|
-
attribute :value,
|
|
26
|
-
attribute :attributes, "Lutaml::Lml::TopElementAttribute", collection: true, default: []
|
|
24
|
+
attribute :properties, "Lutaml::Lml::TopElementAttribute", collection: true, default: -> { [] }
|
|
25
|
+
attribute :value, LiteralValue
|
|
26
|
+
attribute :attributes, "Lutaml::Lml::TopElementAttribute", collection: true, default: -> { [] }
|
|
27
27
|
attribute :extended, :boolean
|
|
28
|
-
attribute :instances, "Lutaml::Lml::Instance", collection: true, default: []
|
|
28
|
+
attribute :instances, "Lutaml::Lml::Instance", collection: true, default: -> { [] }
|
|
29
|
+
|
|
30
|
+
# Declared in full because a mapping block replaces the defaults. The
|
|
31
|
+
# rules below are the defaults, 1:1, except for `value`.
|
|
32
|
+
#
|
|
33
|
+
# `value` needs custom methods because a list literal reaches the model
|
|
34
|
+
# as a bare Array, and lutaml-model refuses one on a non-collection
|
|
35
|
+
# attribute: Attribute#cast splits it, then valid_collection! raises
|
|
36
|
+
# CollectionTrueMissingError. A custom `from` returns before that check,
|
|
37
|
+
# so a non-empty list survives a round trip without changing the
|
|
38
|
+
# serialized form. An empty list still does not: the transform skips a
|
|
39
|
+
# blank value before reaching the custom method, so `[]` reloads as nil.
|
|
40
|
+
key_value do
|
|
41
|
+
map "name", to: :name
|
|
42
|
+
map "visibility", to: :visibility
|
|
43
|
+
map "type", to: :type
|
|
44
|
+
map "id", to: :id
|
|
45
|
+
map "contain", to: :contain
|
|
46
|
+
map "static", to: :static
|
|
47
|
+
map "cardinality", to: :cardinality
|
|
48
|
+
map "keyword", to: :keyword
|
|
49
|
+
map "is_derived", to: :is_derived
|
|
50
|
+
map "is_static", to: :is_static
|
|
51
|
+
map "is_read_only", to: :is_read_only
|
|
52
|
+
map "stereotype", to: :stereotype
|
|
53
|
+
map "definition", to: :definition
|
|
54
|
+
map "association", to: :association
|
|
55
|
+
map "default", to: :default
|
|
56
|
+
map "properties", to: :properties
|
|
57
|
+
map "value", to: :value, with: { to: :value_to, from: :value_from }
|
|
58
|
+
map "attributes", to: :attributes
|
|
59
|
+
map "extended", to: :extended
|
|
60
|
+
map "instances", to: :instances
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Public because lutaml-model invokes mapping methods via public_send.
|
|
64
|
+
#
|
|
65
|
+
# Skipping nil keeps the key out of the output, which is what the default
|
|
66
|
+
# mapping does. `false` is a real literal, so the guard tests for nil
|
|
67
|
+
# rather than truthiness.
|
|
68
|
+
def value_to(model, doc)
|
|
69
|
+
literal = model.value
|
|
70
|
+
return if literal.nil?
|
|
71
|
+
|
|
72
|
+
doc["value"] = unwrap_literal(literal)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# lutaml-model runs this on a throwaway mapper instance, so `self` is not
|
|
76
|
+
# the object being deserialized - `model` is. Writing to `self.value`
|
|
77
|
+
# here would update an object that is discarded a moment later.
|
|
78
|
+
def value_from(model, value)
|
|
79
|
+
model.value = value
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# A LiteralValue sits at the top level for a map literal, and inside the
|
|
85
|
+
# array for a list of references. Writing one through unwrapped raises
|
|
86
|
+
# ArgumentError in the adapter.
|
|
87
|
+
def unwrap_literal(value)
|
|
88
|
+
return value.map { |item| unwrap_literal(item) } if value.is_a?(::Array)
|
|
89
|
+
|
|
90
|
+
value.is_a?(LiteralValue) ? value.value : value
|
|
91
|
+
end
|
|
29
92
|
end
|
|
30
93
|
end
|
|
31
94
|
end
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Lml
|
|
5
5
|
class UmlClass < Lutaml::Model::Serializable
|
|
6
|
-
# From TopElement
|
|
6
|
+
# From TopElement. TextType unescapes \{ \} and normalizes lines
|
|
7
|
+
# on every assignment (builder, yaml, direct).
|
|
7
8
|
attribute :name, :string
|
|
8
|
-
attribute :definition,
|
|
9
|
+
attribute :definition, Lutaml::Lml::Types::TextType
|
|
9
10
|
attribute :keyword, :string
|
|
10
11
|
attribute :stereotype, :string, collection: true, default: -> { [] }
|
|
11
12
|
attribute :visibility, :string, default: "public"
|
|
@@ -31,9 +32,7 @@ module Lutaml
|
|
|
31
32
|
map "name", to: :name
|
|
32
33
|
map "keyword", to: :keyword
|
|
33
34
|
map "is_abstract", to: :is_abstract
|
|
34
|
-
map "definition", to: :definition
|
|
35
|
-
to: :definition_to_yaml, from: :definition_from_yaml
|
|
36
|
-
}
|
|
35
|
+
map "definition", to: :definition
|
|
37
36
|
map "modifier", to: :modifier
|
|
38
37
|
map "stereotype", to: :stereotype
|
|
39
38
|
map "visibility", to: :visibility
|
|
@@ -42,6 +41,9 @@ module Lutaml
|
|
|
42
41
|
map "operations", to: :operations
|
|
43
42
|
map "constraints", to: :constraints
|
|
44
43
|
map "data_types", to: :data_types
|
|
44
|
+
# Owner-side defaulting is a parent-context rule ("an
|
|
45
|
+
# association nested under a class defaults its owner to that
|
|
46
|
+
# class"); only a mapping proc can see the containing model.
|
|
45
47
|
map "associations", to: :associations, with: {
|
|
46
48
|
to: :associations_to_yaml, from: :associations_from_yaml
|
|
47
49
|
}
|
|
@@ -63,22 +65,13 @@ module Lutaml
|
|
|
63
65
|
model.associations = associations
|
|
64
66
|
end
|
|
65
67
|
|
|
66
|
-
def definition_to_yaml(model, doc)
|
|
67
|
-
doc["definition"] = model.definition if model.definition
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def definition_from_yaml(model, value)
|
|
71
|
-
model.definition = value.to_s
|
|
72
|
-
.gsub(/\\}/, "}")
|
|
73
|
-
.gsub(/\\{/, "{")
|
|
74
|
-
.split("\n")
|
|
75
|
-
.map(&:strip)
|
|
76
|
-
.join("\n")
|
|
77
|
-
end
|
|
78
|
-
|
|
79
68
|
def self.entity_type
|
|
80
69
|
:classes
|
|
81
70
|
end
|
|
71
|
+
|
|
72
|
+
def self.classifiable?
|
|
73
|
+
true
|
|
74
|
+
end
|
|
82
75
|
end
|
|
83
76
|
end
|
|
84
77
|
end
|
data/lib/lutaml/lml/parser.rb
CHANGED
|
@@ -5,18 +5,13 @@ require "parslet/convenience"
|
|
|
5
5
|
|
|
6
6
|
module Lutaml
|
|
7
7
|
module Lml
|
|
8
|
+
# The Parslet parser: raw LML text → parse tree. Pipeline-level
|
|
9
|
+
# entry points live on the Lutaml::Lml module (parse /
|
|
10
|
+
# parse_document).
|
|
8
11
|
class Parser < Parslet::Parser
|
|
9
12
|
include Grammar::Full
|
|
10
13
|
|
|
11
14
|
root(:diagram)
|
|
12
|
-
|
|
13
|
-
def self.parse(io)
|
|
14
|
-
Pipeline.call(io)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def self.parse_document(io)
|
|
18
|
-
Pipeline.call(io, resolve: false)
|
|
19
|
-
end
|
|
20
15
|
end
|
|
21
16
|
end
|
|
22
17
|
end
|
data/lib/lutaml/lml/pipeline.rb
CHANGED
|
@@ -5,26 +5,24 @@ require "parslet/convenience"
|
|
|
5
5
|
|
|
6
6
|
module Lutaml
|
|
7
7
|
module Lml
|
|
8
|
+
# Parse pipeline only: preprocess → parse → transform → process →
|
|
9
|
+
# build. View resolution and label enrichment are a separate,
|
|
10
|
+
# explicit post-parse step (ViewResolution) — file I/O for imports
|
|
11
|
+
# does not belong inside parsing.
|
|
8
12
|
class Pipeline
|
|
9
|
-
def self.call(input
|
|
10
|
-
new(input
|
|
13
|
+
def self.call(input)
|
|
14
|
+
new(input).call
|
|
11
15
|
end
|
|
12
16
|
|
|
13
|
-
def initialize(input
|
|
14
|
-
@input = input
|
|
15
|
-
@resolve = resolve
|
|
17
|
+
def initialize(input)
|
|
18
|
+
@input = Source.wrap(input)
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
def call
|
|
19
22
|
data = Preprocessor.call(@input)
|
|
20
23
|
hash = parse_raw(data)
|
|
21
24
|
hash = DataProcessor.process(hash)
|
|
22
|
-
|
|
23
|
-
if @resolve
|
|
24
|
-
document = resolve_document(document)
|
|
25
|
-
AssociationLabelResolver.new.enrich(document)
|
|
26
|
-
end
|
|
27
|
-
document
|
|
25
|
+
build_document(hash)
|
|
28
26
|
end
|
|
29
27
|
|
|
30
28
|
private
|
|
@@ -40,25 +38,6 @@ module Lutaml
|
|
|
40
38
|
def build_document(hash)
|
|
41
39
|
DocumentBuilder.new(DocumentBuilder::DEFAULT_REGISTRY).build(:document, hash)
|
|
42
40
|
end
|
|
43
|
-
|
|
44
|
-
def resolve_document(document)
|
|
45
|
-
return document unless document.view_imports&.any?
|
|
46
|
-
|
|
47
|
-
base_path = @input.is_a?(StringIO) ? nil : @input.path
|
|
48
|
-
entities, associations = ImportResolver.new(base_path).resolve(document)
|
|
49
|
-
entities, associations = ViewResolver.new.resolve(document, entities, associations)
|
|
50
|
-
rebuild_document(document, entities, associations)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def rebuild_document(document, entities, associations)
|
|
54
|
-
grouped = entities.group_by { |e| e.class.entity_type }
|
|
55
|
-
|
|
56
|
-
document.classes = grouped[:classes] || []
|
|
57
|
-
document.enums = grouped[:enums] || []
|
|
58
|
-
document.data_types = grouped[:data_types] || []
|
|
59
|
-
document.associations = associations
|
|
60
|
-
document
|
|
61
|
-
end
|
|
62
41
|
end
|
|
63
42
|
end
|
|
64
43
|
end
|
|
@@ -6,7 +6,7 @@ module Lutaml
|
|
|
6
6
|
attr_reader :input_file
|
|
7
7
|
|
|
8
8
|
def initialize(input_file)
|
|
9
|
-
@input_file = input_file
|
|
9
|
+
@input_file = Source.wrap(input_file)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
class << self
|
|
@@ -16,37 +16,42 @@ module Lutaml
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def call
|
|
19
|
-
input_file.
|
|
20
|
-
include_root = input_file.is_a?(StringIO) ? Dir.pwd : File.dirname(input_file.path)
|
|
21
|
-
input_file.read.split("\n").reduce([]) do |res, line|
|
|
22
|
-
res.push(*process_dsl_line(include_root, line))
|
|
23
|
-
end.join("\n")
|
|
19
|
+
expand_lines(@input_file.read, @input_file.base_dir, [])
|
|
24
20
|
end
|
|
25
21
|
|
|
26
22
|
private
|
|
27
23
|
|
|
28
|
-
def
|
|
29
|
-
|
|
24
|
+
def expand_lines(text, base_dir, chain)
|
|
25
|
+
text.split(/\r?\n/)
|
|
26
|
+
.flat_map { |line| expand_line(line, base_dir, chain) }
|
|
27
|
+
.join("\n")
|
|
30
28
|
end
|
|
31
29
|
|
|
32
|
-
def
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
def expand_line(line, base_dir, chain)
|
|
31
|
+
cleaned = strip_comment(line)
|
|
32
|
+
path = include_path(cleaned, base_dir)
|
|
33
|
+
return [cleaned] unless path
|
|
35
34
|
|
|
36
|
-
|
|
35
|
+
raise Error, "circular include detected: #{path}" if chain.include?(path)
|
|
36
|
+
|
|
37
|
+
expand_lines(read_included(path), File.dirname(path), chain + [path])
|
|
37
38
|
end
|
|
38
39
|
|
|
39
|
-
def
|
|
40
|
-
|
|
41
|
-
return
|
|
40
|
+
def include_path(line, base_dir)
|
|
41
|
+
match = line.match(/^\s*include\s+(.+)/)
|
|
42
|
+
return nil unless match
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
File.expand_path(match[1].strip, base_dir)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def read_included(path)
|
|
48
|
+
File.read(path)
|
|
47
49
|
rescue Errno::ENOENT, Errno::EACCES => e
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
raise Error, "cannot read include #{path}: #{e.message}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def strip_comment(line)
|
|
54
|
+
line.sub(%r{//.*}, "")
|
|
50
55
|
end
|
|
51
56
|
end
|
|
52
57
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'delegate'
|
|
4
|
+
|
|
5
|
+
module Lutaml
|
|
6
|
+
module Lml
|
|
7
|
+
# Normalizes pipeline input (String, StringIO, File, Tempfile) behind
|
|
8
|
+
# one interface: full text, filesystem path (or nil), and the
|
|
9
|
+
# directory imports/includes resolve against.
|
|
10
|
+
class Source
|
|
11
|
+
attr_reader :input
|
|
12
|
+
|
|
13
|
+
def self.wrap(input)
|
|
14
|
+
input.is_a?(Source) ? input : new(input)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(input)
|
|
18
|
+
@input = input
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def read
|
|
22
|
+
return @input if @input.is_a?(String)
|
|
23
|
+
|
|
24
|
+
@input.rewind
|
|
25
|
+
@input.read
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Filesystem path, or nil for anonymous input (String/StringIO).
|
|
29
|
+
def path
|
|
30
|
+
io = unwrapped
|
|
31
|
+
io.is_a?(File) ? io.path : nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def base_dir
|
|
35
|
+
path ? File.dirname(path) : Dir.pwd
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# Tempfile delegates to File (it is not a File subclass on
|
|
41
|
+
# Ruby >= 3.4), so unwrap before type checks.
|
|
42
|
+
def unwrapped
|
|
43
|
+
io = @input
|
|
44
|
+
io = io.__getobj__ while io.is_a?(Delegator)
|
|
45
|
+
io
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/lutaml/lml/transform.rb
CHANGED
|
@@ -14,7 +14,11 @@ module Lutaml
|
|
|
14
14
|
rule(visibility_modifier: simple(:visibility_value)) do
|
|
15
15
|
VISIBILITY_MAP.fetch(visibility_value.to_s, 'public')
|
|
16
16
|
end
|
|
17
|
-
|
|
17
|
+
# Global scalar normalizer: Parslet applies this to every leaf in
|
|
18
|
+
# the parse tree (names may legally contain trailing spaces via
|
|
19
|
+
# class_name_chars). Binding is named :value to make the breadth
|
|
20
|
+
# explicit — it is not tied to any one grammar rule.
|
|
21
|
+
rule(simple(:value)) { value.nil? ? value : value.to_s.strip }
|
|
18
22
|
end
|
|
19
23
|
end
|
|
20
24
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'lutaml/model'
|
|
4
|
+
|
|
5
|
+
module Lutaml
|
|
6
|
+
module Lml
|
|
7
|
+
module Types
|
|
8
|
+
# LML `definition` body text. The grammar allows literal braces to
|
|
9
|
+
# be escaped (\{ \}) so they do not close the definition block;
|
|
10
|
+
# casting unescapes them and normalizes line indentation. One
|
|
11
|
+
# implementation shared by every attribute declared with this
|
|
12
|
+
# type — no per-model (de)serialization procs.
|
|
13
|
+
class TextType < Lutaml::Model::Type::String
|
|
14
|
+
def self.cast(value, options = {})
|
|
15
|
+
text = super
|
|
16
|
+
return nil if text.nil?
|
|
17
|
+
return text if Lutaml::Model::Utils.uninitialized?(text)
|
|
18
|
+
|
|
19
|
+
text.gsub(/\\}/, '}')
|
|
20
|
+
.gsub(/\\{/, '{')
|
|
21
|
+
.split("\n")
|
|
22
|
+
.map(&:strip)
|
|
23
|
+
.join("\n")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/lutaml/lml/version.rb
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Lml
|
|
5
|
+
# Post-parse step: expand view imports, apply show/hide filters,
|
|
6
|
+
# rebuild the document's entity collections, and enrich association
|
|
7
|
+
# labels. Import expansion runs only when the document declares
|
|
8
|
+
# view imports; label enrichment always runs.
|
|
9
|
+
class ViewResolution
|
|
10
|
+
def self.call(document, base_dir)
|
|
11
|
+
document = resolve_imports(document, base_dir) if document.view_imports.any?
|
|
12
|
+
AssociationLabelResolver.new.enrich(document)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.resolve_imports(document, base_dir)
|
|
16
|
+
entity_index, associations = ImportResolver.new(base_dir).resolve(document)
|
|
17
|
+
entities, associations = ViewResolver.new.resolve(document, entity_index, associations)
|
|
18
|
+
rebuild_document(document, entities, associations)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.rebuild_document(document, entities, associations)
|
|
22
|
+
grouped = entities.group_by { |e| e.class.entity_type }
|
|
23
|
+
|
|
24
|
+
EntityTypes.symbols.each do |symbol|
|
|
25
|
+
document.public_send("#{symbol}=", grouped[symbol] || [])
|
|
26
|
+
end
|
|
27
|
+
document.associations = associations
|
|
28
|
+
document
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Lml
|
|
5
5
|
class ViewResolver
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
# entity_index is the ImportResolver's name → entity map.
|
|
7
|
+
def resolve(document, entity_index, associations)
|
|
8
|
+
visible = apply_filters(document, entity_index)
|
|
8
9
|
filtered_associations = filter_associations(associations, visible)
|
|
9
10
|
|
|
10
11
|
[visible, filtered_associations]
|
|
@@ -12,9 +13,8 @@ module Lutaml
|
|
|
12
13
|
|
|
13
14
|
private
|
|
14
15
|
|
|
15
|
-
def apply_filters(document,
|
|
16
|
-
names =
|
|
17
|
-
|
|
16
|
+
def apply_filters(document, entity_index)
|
|
17
|
+
names = entity_index.reject { |name, _| name.nil? }
|
|
18
18
|
names = apply_show_filter(names, document.show_filter)
|
|
19
19
|
names = apply_hide_filter(names, document.hide_filter)
|
|
20
20
|
|
data/lib/lutaml/lml.rb
CHANGED
|
@@ -14,27 +14,47 @@ module Lutaml
|
|
|
14
14
|
module Lml
|
|
15
15
|
class Error < Lutaml::Error; end
|
|
16
16
|
class ParsingError < Error; end
|
|
17
|
+
class ImportError < Error; end
|
|
17
18
|
|
|
18
19
|
def self.compile(input, namespace: nil)
|
|
19
20
|
ModelCompiler.new(namespace: namespace).compile(input)
|
|
20
21
|
end
|
|
21
22
|
|
|
23
|
+
# Full entry point: parse, then resolve view imports and enrich
|
|
24
|
+
# association labels.
|
|
25
|
+
def self.parse(input)
|
|
26
|
+
source = Source.wrap(input)
|
|
27
|
+
document = Pipeline.call(source)
|
|
28
|
+
ViewResolution.call(document, source.base_dir)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Parse only — no import resolution, no label enrichment.
|
|
32
|
+
def self.parse_document(input)
|
|
33
|
+
Pipeline.call(input)
|
|
34
|
+
end
|
|
35
|
+
|
|
22
36
|
# Top-level autoloads
|
|
23
37
|
autoload :Parser, "lutaml/lml/parser"
|
|
24
38
|
autoload :Pipeline, "lutaml/lml/pipeline"
|
|
25
39
|
autoload :Preprocessor, "lutaml/lml/preprocessor"
|
|
40
|
+
autoload :Source, "lutaml/lml/source"
|
|
26
41
|
autoload :Transform, "lutaml/lml/transform"
|
|
27
42
|
autoload :DataProcessor, "lutaml/lml/data_processor"
|
|
28
43
|
autoload :DocumentBuilder, "lutaml/lml/document_builder"
|
|
29
44
|
autoload :ModelCompiler, "lutaml/lml/model_compiler"
|
|
30
45
|
autoload :ImportResolver, "lutaml/lml/import_resolver"
|
|
31
46
|
autoload :ViewResolver, "lutaml/lml/view_resolver"
|
|
47
|
+
autoload :ViewResolution, "lutaml/lml/view_resolution"
|
|
48
|
+
autoload :EntityTypes, "lutaml/lml/entity_types"
|
|
32
49
|
autoload :AssociationLabelResolver, "lutaml/lml/association_label_resolver"
|
|
33
50
|
autoload :Executor, "lutaml/lml/executor"
|
|
34
51
|
autoload :YamlParser, "lutaml/lml/yaml_parser"
|
|
35
52
|
autoload :HasAttributes, "lutaml/lml/has_attributes"
|
|
36
53
|
autoload :VERSION, "lutaml/lml/version"
|
|
37
54
|
|
|
55
|
+
# Type classes (Lutaml::Model::Type::Value subclasses, not models)
|
|
56
|
+
autoload :LiteralValue, "lutaml/lml/literal_value"
|
|
57
|
+
|
|
38
58
|
# Model classes (in Lutaml::Lml namespace, files in models/ directory)
|
|
39
59
|
autoload :Action, "lutaml/lml/models/action"
|
|
40
60
|
autoload :Association, "lutaml/lml/models/association"
|
|
@@ -64,5 +84,6 @@ module Lutaml
|
|
|
64
84
|
# Namespaces with their own autoloads
|
|
65
85
|
autoload :Grammar, "lutaml/lml/grammar"
|
|
66
86
|
autoload :Format, "lutaml/lml/format"
|
|
87
|
+
autoload :Types, "lutaml/lml/types"
|
|
67
88
|
end
|
|
68
89
|
end
|