lutaml-lml 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/lib/lutaml/lml/cli.rb +84 -18
  3. data/lib/lutaml/lml/data_processor/value_processing.rb +6 -1
  4. data/lib/lutaml/lml/document_builder.rb +72 -3
  5. data/lib/lutaml/lml/format/adapter/mapping.rb +12 -0
  6. data/lib/lutaml/lml/format/adapter/standard_adapter.rb +41 -12
  7. data/lib/lutaml/lml/formatter/elk.rb +45 -0
  8. data/lib/lutaml/lml/formatter/graphviz/node_formatter.rb +2 -1
  9. data/lib/lutaml/lml/formatter/svg_pdf/path_parser.rb +134 -0
  10. data/lib/lutaml/lml/formatter/svg_pdf.rb +281 -0
  11. data/lib/lutaml/lml/formatter.rb +2 -0
  12. data/lib/lutaml/lml/grammar/lml.artifact.json +7265 -4550
  13. data/lib/lutaml/lml/grammar/lml.parg +230 -115
  14. data/lib/lutaml/lml/layout/elk_engine.rb +15 -0
  15. data/lib/lutaml/lml/layout/elk_graph_builder.rb +129 -0
  16. data/lib/lutaml/lml/layout/elk_svg_markers.rb +45 -0
  17. data/lib/lutaml/lml/layout/elk_svg_renderer.rb +119 -0
  18. data/lib/lutaml/lml/layout/graph_viz_engine.rb +1 -1
  19. data/lib/lutaml/lml/layout.rb +4 -0
  20. data/lib/lutaml/lml/model_compiler/enums.rb +172 -0
  21. data/lib/lutaml/lml/model_compiler/hydration.rb +123 -0
  22. data/lib/lutaml/lml/model_compiler/serialization_mappings.rb +162 -0
  23. data/lib/lutaml/lml/model_compiler/string_formats.rb +42 -0
  24. data/lib/lutaml/lml/model_compiler/validation.rb +65 -0
  25. data/lib/lutaml/lml/model_compiler.rb +46 -453
  26. data/lib/lutaml/lml/models/document.rb +29 -0
  27. data/lib/lutaml/lml/models/operation_parameter.rb +1 -0
  28. data/lib/lutaml/lml/models/reference.rb +23 -0
  29. data/lib/lutaml/lml/models/top_element_attribute.rb +12 -0
  30. data/lib/lutaml/lml/models/uml_class.rb +1 -1
  31. data/lib/lutaml/lml/preprocessor.rb +44 -79
  32. data/lib/lutaml/lml/transform.rb +54 -6
  33. data/lib/lutaml/lml/validator.rb +88 -19
  34. data/lib/lutaml/lml/version.rb +1 -1
  35. data/lib/lutaml/lml.rb +19 -3
  36. metadata +17 -4
@@ -10,139 +10,241 @@
10
10
  # `[ x as k ]` yields k: nil when absent (Parslet omits the key —
11
11
  # consumers treat nil and absent as equivalent).
12
12
  grammar Lml version "1.0.0" {
13
+ # ---- Trivia (parsanol-ruby#134 skip rule) ----
14
+ # Grammar-native trivia replaces the pre-parse comment stripping:
15
+ # `//` line and `/* */` block comments (RS 3001 par. Comment) plus
16
+ # blanks/tabs are consumed and ignored between tokens. Newlines stay
17
+ # grammar-visible (line-oriented rules keep their semantics; the
18
+ # preprocessor preserved them too). Atomic token rules keep string
19
+ # literals and identifiers contiguous.
20
+ skip = trivia
21
+ trivia = 1*( ( 1*( " " / %x09 ) ) / ( line_comment [ ( [ %x0D ] %x0A / %x0D ) ] ) / block_comment )
22
+ line_comment = "//" ( *( !newline any_char ) )
23
+ block_comment = "/*" ( *( !"*/" any_char ) ) "*/"
24
+ atomic kw_association_sp = "association" 1*" "
25
+ atomic kw_artifact_sp = "artifact" 1*" "
26
+ atomic kw_attribute_sp = "attribute" 1*" "
27
+ atomic kw_caption_sp = "caption" 1*" "
28
+ atomic kw_class_sp = "class" 1*" "
29
+ atomic kw_visibility_sp = "visibility" 1*" "
30
+ atomic kw_version_sp = "version" 1*" "
31
+ version_declaration = kw_version_sp quoted_string as version
32
+ atomic kw_abstract_sp = "abstract" 1*" "
33
+ atomic kw_collection_sp = "collection" 1*" "
34
+ atomic kw_condition_sp = "condition" 1*" "
35
+ atomic kw_data_type_sp = "data_type" 1*" "
36
+ atomic kw_definition_sp = "definition" 1*" "
37
+ atomic kw_derive_sp = "derive" 1*" "
38
+ atomic kw_element_sp = "element" 1*" "
39
+ atomic kw_enum_sp = "enum" 1*" "
40
+ atomic kw_fontname_sp = "fontname" 1*" "
41
+ atomic kw_format_sp = "format" 1*" "
42
+ atomic kw_hide_sp = "hide" 1*" "
43
+ atomic kw_import_sp = "import" 1*" "
44
+ atomic kw_instance_sp = "instance" 1*" "
45
+ atomic kw_isa_sp = "isa" 1*" "
46
+ atomic kw_map_sp = "map" 1*" "
47
+ atomic kw_map_attribute_sp = "map_attribute" 1*" "
48
+ atomic kw_map_content_sp = "map_content" 1*" "
49
+ atomic kw_map_element_sp = "map_element" 1*" "
50
+ atomic kw_map_key_sp = "map_key" 1*" "
51
+ atomic kw_map_value_sp = "map_value" 1*" "
52
+ atomic kw_mapping_sp = "mapping" 1*" "
53
+ atomic kw_member_sp = "member" 1*" "
54
+ atomic kw_member_type_sp = "member_type" 1*" "
55
+ atomic kw_namespace_sp = "namespace" 1*" "
56
+ atomic kw_owner_sp = "owner" 1*" "
57
+ atomic kw_owner_type_sp = "owner_type" 1*" "
58
+ atomic kw_package_sp = "package" 1*" "
59
+ atomic kw_pattern_sp = "pattern" 1*" "
60
+ atomic kw_prefix_sp = "prefix" 1*" "
61
+ atomic kw_primitive_sp = "primitive" 1*" "
62
+ atomic kw_require_sp = "require" 1*" "
63
+ atomic kw_show_sp = "show" 1*" "
64
+ atomic kw_string_format_sp = "string_format" 1*" "
65
+ atomic kw_title_sp = "title" 1*" "
66
+ atomic kw_to_instance_sp = "to_instance" 1*" "
67
+ atomic kw_uri_sp = "uri" 1*" "
68
+ atomic kw_value_sp = "value" 1*" "
69
+ atomic kw_values_sp = "values" 1*" "
13
70
  # ---- Character primitives ----
14
71
  alpha = %x41-5A / %x61-7A
15
- word = %x41-5A / %x61-7A / %x30-39 / "_"
72
+ atomic word = %x41-5A / %x61-7A / %x30-39 / "_"
16
73
  cardchar = %x30-39 / %x61-7A / "*"
17
74
  # Parslet's `any`: any Unicode character (not just bytes)
18
75
  any_char = %x00-10FFFF
19
76
  # [^\s\n\r] — regex \s covers space, tab, CR, LF, VT, FF
20
- nonspace = !" " !%x09 !%x0A !%x0B !%x0C !%x0D any_char
77
+ atomic nonspace = !" " !%x09 !%x0A !%x0B !%x0C !%x0D any_char
21
78
  quote = %x22 / %x27
22
79
  newline = %x0D / %x0A
23
80
 
24
- ws = 1*" "
25
- whitespace = 1*( " " / %x09 / [ %x0D ] %x0A / %x0D / ";" )
81
+ whitespace = 1*( ( 1*( " " / %x09 ) ) / ( [ %x0D ] %x0A ) / %x0D / ";" / line_comment / block_comment )
26
82
 
27
83
  # match['"\'']
28
84
  quotes = %x22 / %x27
29
85
 
30
- # quoted('"', :string) | quoted("'", :string)
31
- dq_string = %x22 ( *( !%x22 any_char ) ) as string %x22
32
- sq_string = %x27 ( *( !%x27 any_char ) ) as string %x27
86
+ # RS 3001 §String values: exactly two escape sequences, \" and \\,
87
+ # strict in both quote kinds; a backslash before anything else is not
88
+ # a string. atomic keeps the scan verbatim (skip trivia must not eat
89
+ # interior runs).
90
+ atomic dq_string = %x22 ( *( ( %x5C %x22 ) / ( %x5C %x5C ) / ( !( %x22 / %x5C ) %x00-10FFFF ) ) ) as string %x22
91
+ atomic sq_string = %x27 ( *( ( %x5C %x27 ) / ( %x5C %x5C ) / ( !( %x27 / %x5C ) %x00-10FFFF ) ) ) as string %x27
33
92
  quoted_string = dq_string / sq_string
34
93
  # any_quoted(:path) for view import
35
- dq_path = %x22 ( *( !%x22 any_char ) ) as path %x22
36
- sq_path = %x27 ( *( !%x27 any_char ) ) as path %x27
94
+ atomic dq_path = %x22 until "\"" as path %x22
95
+ atomic sq_path = %x27 until "'" as path %x27
37
96
 
38
97
  boolean = ( "true" / "false" ) as boolean
98
+ null_literal = "null" as null
39
99
  number = ( 1*%x30-39 "." 1*%x30-39 ) as float / ( 1*%x30-39 ) as number
40
100
  variable = quoted_string / 1*word
41
101
  reference = ( "reference:(" / "ref:(" ) ( variable *( "." variable ) ) as reference ")"
42
102
  range = ( variable as start ".." variable as end ) as range
43
103
  namespaced_identifier = variable *( "::" variable )
44
- comment_definition = [ ws ] ( "**" / "#" ) ( *( !newline any_char ) ) as comments
45
- comment_multiline_definition = [ ws ] "*|" ( *( !"|*" any_char ) ) as comments [ whitespace ] "|*"
104
+ comment_definition = ( "**" / "#" ) ( *( !newline ANY ) ) as comments
105
+ comment_multiline_definition = "*|" until "|*" as comments [ whitespace ] "|*"
46
106
 
47
- class_name_chars = 1*( %x41-5A / %x61-7A / %x30-39 / " " / "_" / "-" / ":" / "." )
107
+ # Spaces are interior-only in name and type tokens: a run may be
108
+ # followed by space-then-run, never a trailing space. The trailing
109
+ # separator is left for skip trivia, so captures need no post-strip
110
+ # and quoted string values keep their padding verbatim (RS 3001
111
+ # §String values).
112
+ # Atomic composite: the interior %x20 must match verbatim — skip
113
+ # trivia injects before sequence children and would otherwise eat the
114
+ # space before the grammar's own literal can (the type_token
115
+ # precedent).
116
+ atomic class_run = 1*( %x41-5A / %x61-7A / %x30-39 / "_" / "-" / ":" / "." )
117
+ atomic class_name_chars = class_run *( %x20 class_run )
48
118
  class_name = class_name_chars [ "(" class_name_chars ")" ]
49
119
  cardinality_body_definition = cardchar as min [ ".." [ cardchar as max ] ]
50
120
  cardinality = "[" cardinality_body_definition as cardinality "]"
51
121
 
52
- data_value = boolean / reference / range / number / quoted_string
53
- key_value_pair = variable as key ws [ "=" ] [ ws ] data_value as value
54
- key_value_map = "{" [ whitespace ] ( *( key_value_pair whitespace ) ) as key_value_map "}"
122
+ data_value = boolean / null_literal / reference / range / number / quoted_string
123
+ key_value_pair = variable as key [ "=" ] data_value as value
124
+ key_value_map = "{" [ whitespace ] ( *( key_value_pair [ whitespace ] ) ) as key_value_map "}"
55
125
  kw_visibility_modifier = "+" / "-" / "#" / "~"
56
126
 
57
127
  # ---- Attributes ----
58
- attribute = comment_definition / ( variable as key [ ws ] [ "+" as add ] [ "=" ] [ ws ] attribute_value as value )
59
- attributes = ( *( attribute_line / whitespace ) ) as attributes
60
- attribute_line = [ ws ] attribute [ [ "," ] whitespace ]
128
+ attribute = comment_definition / ( variable as key [ "+" as add ] [ "=" ] attribute_value as value )
129
+ attributes = ( *( definition_body / definition_line / attribute_line / whitespace ) ) as attributes
130
+ attribute_line = attribute [ [ "," ] whitespace ]
61
131
  visibility = kw_visibility_modifier as visibility_modifier
62
132
  attribute_stereotype = "<<" ( 1*( %x41-5A / %x61-7A / %x30-39 / "_" / "-" / "/" ) ) as stereotype ">>"
63
- typechar = %x41-5A / %x61-7A / %x30-39 / "_" / "-" / " " / ":" / "/" / "+"
64
- namechar = %x41-5A / %x61-7A / %x30-39 / "_" / "-" / "/" / "+"
65
- attribute_type = [ ":" ] [ ws ] [ attribute_stereotype ] [ ws ] [ quotes as type_literal ] ( 1*typechar ) as type [ quotes ] [ ws ]
133
+ atomic type_run = 1*( !( "//" / "/*" ) ( %x41-5A / %x61-7A / %x30-39 / "_" / "-" / ":" / "/" / "+" ) )
134
+ atomic type_token = type_run *( %x20 type_run )
135
+ atomic namechar = %x41-5A / %x61-7A / %x30-39 / "_" / "-" / "/" / "+"
136
+ attribute_type = [ ":" ] [ attribute_stereotype ] [ quotes as type_literal ] ( type_token ) as type [ quotes ]
66
137
  attribute_name = ( 1*namechar ) as name
67
- attribute_definition_name = ( quotes ( 1*( %x41-5A / %x61-7A / %x30-39 / "_" / "-" / " " / "/" / "+" ) ) as name quotes ) / attribute_name
68
- attribute_definition = ( [ visibility ] as visibility [ ws ] attribute_definition_name [ ws ] [ attribute_type ] [ cardinality ] [ class_body ] ) as attributes
69
-
70
- keyword_type_argument = "type" [ ws ] ( 1*nonspace ) as type [ whitespace ]
71
- keyword_cardinality_argument = "cardinality" [ ws ] cardinality_body_definition as cardinality [ whitespace ]
72
- keyword_any_argument = [ ws ] ( 1*nonspace ) as name ws [ "=" / ":" ] [ ws ] attribute_value as value [ whitespace ]
73
- keyword_values_argument = "values" ws ( ( value_set_body ) as value_set / ( variable ) as value_set_ref ) [ whitespace ]
74
- keyword_pattern_argument = ( "pattern" ws "/" ( *( ( "\\" any_char ) / ( !"/" any_char ) ) ) as pattern "/" [ whitespace ] )
138
+ atomic quoted_name_run = 1*( %x41-5A / %x61-7A / %x30-39 / "_" / "-" / "/" / "+" )
139
+ atomic quoted_name_body = quoted_name_run *( %x20 quoted_name_run )
140
+ attribute_definition_name = ( quotes ( quoted_name_body ) as name quotes ) / attribute_name
141
+ attribute_definition = ( [ visibility ] as visibility attribute_definition_name [ attribute_type ] [ cardinality ] [ class_body ] ) as attributes
142
+
143
+ keyword_type_argument = "type" ( 1*nonspace ) as type [ whitespace ]
144
+ keyword_cardinality_argument = "cardinality" cardinality_body_definition as cardinality [ whitespace ]
145
+ keyword_any_argument = ( keyword_any_name ) as name [ "=" / ":" ] attribute_value as value [ whitespace ]
146
+ atomic keyword_any_name = 1*( !( "{" / "}" ) nonspace )
147
+ keyword_values_argument = kw_values_sp ( ( value_set_body ) as value_set / ( variable ) as value_set_ref ) [ whitespace ]
148
+ keyword_pattern_argument = ( kw_pattern_sp "/" ( *( ( "\\" any_char ) / ( !"/" any_char ) ) ) as pattern "/" [ whitespace ] )
75
149
  value_set_body = "{" [ whitespace ] ( *( ( ( data_value / variable ) as item ) [ "," ] [ whitespace ] ) ) as items "}"
76
150
  keyword_attribute_options = *( keyword_type_argument / keyword_cardinality_argument / keyword_values_argument / keyword_pattern_argument / ( keyword_any_argument ) as properties )
77
151
  keyword_attribute_body = "{" [ whitespace ] keyword_attribute_options [ whitespace ] "}"
78
- keyword_attribute_definition = ( "attribute" ws attribute_name [ ws ] keyword_attribute_body ) as attributes
152
+ keyword_attribute_definition = ( kw_attribute_sp attribute_name keyword_attribute_body ) as attributes
79
153
 
80
154
  # ---- Associations ----
81
- owner_cardinality = [ ws ] "[" cardinality_body_definition as owner_end_cardinality "]"
82
- owner_attribute_name = "#" [ visibility ] [ ws ] variable as owner_end_attribute_name [ ws ]
83
- owner_definition = [ whitespace ] "owner" ws variable as owner_end [ owner_attribute_name ] [ owner_cardinality ]
84
- owner_type = [ whitespace ] "owner_type" ws variable as owner_end_type
85
- member_cardinality = [ ws ] "[" cardinality_body_definition as member_end_cardinality "]"
86
- member_attribute_name = "#" [ visibility ] [ ws ] variable as member_end_attribute_name [ ws ]
87
- member_definition = [ whitespace ] "member" ws variable as member_end [ member_attribute_name ] [ member_cardinality ]
88
- member_type = [ whitespace ] "member_type" ws variable as member_end_type
155
+ owner_cardinality = "[" cardinality_body_definition as owner_end_cardinality "]"
156
+ owner_attribute_name = "#" [ visibility ] variable as owner_end_attribute_name
157
+ owner_definition = [ whitespace ] kw_owner_sp variable as owner_end [ owner_attribute_name ] [ owner_cardinality ]
158
+ owner_type = [ whitespace ] kw_owner_type_sp variable as owner_end_type
159
+ member_cardinality = "[" cardinality_body_definition as member_end_cardinality "]"
160
+ member_attribute_name = "#" [ visibility ] variable as member_end_attribute_name
161
+ member_definition = [ whitespace ] kw_member_sp variable as member_end [ member_attribute_name ] [ member_cardinality ]
162
+ member_type = [ whitespace ] kw_member_type_sp variable as member_end_type
89
163
  association_inner_definition = ( owner_type / member_type / owner_definition / member_definition / comment_definition / comment_multiline_definition ) [ whitespace ]
90
- association_body = [ ws ] "{" [ whitespace ] ( *( association_inner_definition ) ) as members "}"
91
- association_definition = [ whitespace ] "association" ws [ variable as name ] [ ws ] association_body
164
+ association_body = "{" [ whitespace ] ( *( association_inner_definition ) ) as members "}"
165
+ association_definition = [ whitespace ] kw_association_sp [ variable as name ] association_body
92
166
 
93
167
  # ---- Definitions ----
94
- class_modifier = [ ( [ whitespace ] ( "abstract" / "interface" ) ) as modifier ws ]
168
+ # ---- Operations (class methods) ----
169
+ # Symbol visibility reuses the attribute prefix (+ - # ~); the
170
+ # transform maps it to public/private/protected/friendly.
171
+ operation_definition = [ whitespace ] [ visibility ] ( 1*word ) as name "(" [ ( operation_params ) as owned_parameter ] ")" [ operation_return ] [ operation_body ] [ whitespace ]
172
+ operation_params = operation_param *( "," [ whitespace ] operation_param )
173
+ operation_return = ":" ( type_token ) as return_type [ cardinality ]
174
+ # A parenthesized alternative is not an Entity, so an optional body
175
+ # of `x as k` around it gets no skip injection - the blank after "="
176
+ # was unconsumable. Reference a rule instead.
177
+ operation_param = ( [ ( "in" / "out" / "inout" ) as direction 1*" " ] ( 1*word ) as name [ ":" ] param_type as type [ "=" ] param_default_value as param_default )
178
+ param_default_value = [ quoted_string / ( 1*word ) / number ]
179
+ # The attribute pattern: separator as its own optional, value as a
180
+ # rule sibling. A `x as k` optional body gets no interior skip
181
+ # injection, so [ ":" type as t ] cannot consume the blank after
182
+ # the separator.
183
+ param_type = [ type_token ]
184
+ operation_body = "{" [ whitespace ] [ ( *( ( 1*word ) as stereotype [ "," ] [ whitespace ] ) ) as stereotype ] [ whitespace ] "}"
185
+
186
+ class_modifier = [ [ whitespace ] modifier_token ]
187
+ atomic modifier_token = ( ( "abstract" / "interface" ) as modifier 1*" " )
188
+ # RS 3001 par. Declaration modifiers: keyword prefix (sugar) and the
189
+ # canonical in-block declarations. The `package` visibility keyword is
190
+ # guarded: it must be followed by a declaration keyword, so a bare
191
+ # `package P { }` still binds as a package declaration.
192
+ atomic element_visibility = ( ( "public" / "private" / "protected" / ( "package" &( 1*" " ( kw_class_sp / kw_enum_sp / kw_data_type_sp / kw_primitive_sp / kw_package_sp ) ) ) ) as visibility 1*" " )
193
+ visibility_declaration = kw_visibility_sp ( "public" / "private" / "protected" / "package" ) as visibility
194
+ abstract_declaration = kw_abstract_sp ( "true" / "false" ) as abstract
95
195
  # ---- Namespaces (RS 3010 extension: lutaml-model XmlNamespace) ----
96
- namespace_uri_line = ( [ ws ] "uri" ws ( quoted_string / ( 1*word ) ) as uri [ whitespace ] )
97
- namespace_prefix_line = ( [ ws ] "prefix" ws ( quoted_string / ( 1*word ) ) as prefix [ whitespace ] )
196
+ namespace_uri_line = ( kw_uri_sp ( quoted_string / ( 1*word ) ) as uri [ whitespace ] )
197
+ namespace_prefix_line = ( kw_prefix_sp ( quoted_string / ( 1*word ) ) as prefix [ whitespace ] )
98
198
  namespace_body = "{" [ whitespace ] ( *( namespace_uri_line / namespace_prefix_line ) ) as members "}"
99
- namespace_inline = ( ws ( quoted_string ) as uri [ ws "prefix" ws ( quoted_string / ( 1*word ) ) as prefix ] )
100
- namespace_definition = ( [ whitespace ] "namespace" ws ( 1*word ) as name [ ws ] ( namespace_body / namespace_inline ) [ whitespace ] ) as namespaces
199
+ namespace_inline = ( ( quoted_string ) as uri [ kw_prefix_sp ( quoted_string / ( 1*word ) ) as prefix ] )
200
+ namespace_definition = ( [ whitespace ] kw_namespace_sp ( 1*word ) as name ( namespace_body / namespace_inline ) [ whitespace ] ) as namespaces
101
201
 
102
202
  # ---- Serialization mappings (RS 3010 extension: sibling-linked) ----
103
203
  serialization_body = "{" [ whitespace ] ( *( serialization_line ) ) as rules "}"
104
204
  serialization_line = ( mapping_element_name_line / mapping_element_line / mapping_attribute_line / mapping_content_line / mapping_namespace_line / mapping_mixed_line / legacy_map_line / mapping_map_line / mapping_map_key_line / mapping_map_value_line ) [ whitespace ]
105
- legacy_map_line = ( [ ws ] "map" ws dq_string as wire "," ws "to:" ws ( quoted_string / ( 1*word ) ) as to [ whitespace ] ) as legacy_map
106
- mapping_element_name_line = ( [ ws ] "element" ws dq_string as element_name [ whitespace ] ) as element_name
107
- mapping_element_line = ( [ ws ] "map_element" ws ( element_args ) ) as map_element
108
- element_args = ( dq_string as wire [ ws ( 1*word ) as field ] / ( 1*word ) as field )
109
- mapping_attribute_line = ( [ ws ] "map_attribute" ws ( attribute_args ) ) as map_attribute
110
- attribute_args = ( dq_string as wire [ ws ( 1*word ) as field ] / ( 1*word ) as field )
111
- mapping_content_line = ( [ ws ] "map_content" ws ( 1*word ) as field ) as map_content
112
- mapping_namespace_line = ( [ ws ] "namespace" ws ( 1*word ) as namespace_ref ) as map_namespace
113
- mapping_mixed_line = ( [ ws ] "mixed_content" ) as mixed_content
114
- mapping_map_line = ( [ ws ] "map" ws map_args ) as map
115
- map_args = ( dq_string as wire ws ( 1*word ) as field / dq_string as wire / ( 1*word ) as field )
116
- mapping_map_key_line = ( [ ws ] "map_key" ws "to_instance" ws ( 1*word ) as field ) as map_key
117
- mapping_map_value_line = ( [ ws ] "map_value" ws ( "to_instance" / "as_attribute" ) as mode ws ( 1*word ) as field ) as map_value
118
- mapping_definition = ( [ whitespace ] "mapping" ws ( 1*word ) as format ws ( 1*word ) as target [ ws ] serialization_body [ whitespace ] ) as serialization_mappings
205
+ legacy_map_line = ( kw_map_sp dq_string as wire "," "to:" ( quoted_string / ( 1*word ) ) as to [ whitespace ] ) as legacy_map
206
+ mapping_element_name_line = ( kw_element_sp dq_string as element_name [ whitespace ] ) as element_name
207
+ mapping_element_line = ( kw_map_element_sp ( element_args ) ) as map_element
208
+ element_args = ( dq_string as wire [ ( 1*word ) as field ] / ( 1*word ) as field )
209
+ mapping_attribute_line = ( kw_map_attribute_sp ( attribute_args ) ) as map_attribute
210
+ attribute_args = ( dq_string as wire [ ( 1*word ) as field ] / ( 1*word ) as field )
211
+ mapping_content_line = ( kw_map_content_sp ( 1*word ) as field ) as map_content
212
+ mapping_namespace_line = ( kw_namespace_sp ( 1*word ) as namespace_ref ) as map_namespace
213
+ mapping_mixed_line = ( "mixed_content" ) as mixed_content
214
+ mapping_map_line = ( kw_map_sp map_args ) as map
215
+ map_args = ( dq_string as wire ( 1*word ) as field / dq_string as wire / ( 1*word ) as field )
216
+ mapping_map_key_line = ( kw_map_key_sp kw_to_instance_sp ( 1*word ) as field ) as map_key
217
+ mapping_map_value_line = ( kw_map_value_sp map_value_mode ( 1*word ) as field ) as map_value
218
+ atomic map_value_mode = ( ( "to_instance" / "as_attribute" ) as mode 1*" " )
219
+ mapping_definition = ( [ whitespace ] kw_mapping_sp ( 1*word ) as format ( 1*word ) as target serialization_body [ whitespace ] ) as serialization_mappings
119
220
 
120
221
  # ---- Mapping blocks (lutaml-model wire names) ----
121
222
  map_name = ( quoted_string / 1*( word / "-" / "." ) )
122
- map_line = [ ws ] "map" ws ( map_name ) as wire "," ws "to:" ws ( map_name ) as to [ whitespace ]
123
- mapping_body = [ ws ] "{" [ whitespace ] ( *( map_line ) ) as maps "}"
124
- class_mapping_section = ( [ whitespace ] "mapping" ws ( 1*word ) as format [ ws ] serialization_body [ whitespace ] ) as serialization_mappings
223
+ map_line = kw_map_sp ( map_name ) as wire "," "to:" ( map_name ) as to [ whitespace ]
224
+ mapping_body = "{" [ whitespace ] ( *( map_line ) ) as maps "}"
225
+ class_mapping_section = ( [ whitespace ] kw_mapping_sp ( 1*word ) as format serialization_body [ whitespace ] ) as serialization_mappings
125
226
 
126
- derive_definition = ( [ whitespace ] "derive" ws attribute_name [ ws ] ":" [ ws ] [ quotes ] ( 1*typechar ) as type [ quotes ] ) as derived_field [ whitespace ]
127
- string_format_item = ( [ ws ] ( "artifact" / "root" ) as key ws ( quoted_string / 1*nonspace ) as value [ whitespace ] )
227
+ derive_definition = ( [ whitespace ] kw_derive_sp attribute_name ":" [ quotes ] ( type_token ) as type [ quotes ] ) as derived_field [ whitespace ]
228
+ string_format_item = ( sf_key_token ( quoted_string / 1*nonspace ) as value [ whitespace ] )
229
+ atomic sf_key_token = ( ( "artifact" / "root" ) as key 1*" " )
128
230
  string_format_body = "{" [ whitespace ] ( *( string_format_item ) ) as items [ whitespace ] "}"
129
- string_format_definition = ( [ whitespace ] "string_format" ws ( variable ) as format [ ws ] string_format_body ) as string_format
231
+ string_format_definition = ( [ whitespace ] kw_string_format_sp ( variable ) as format string_format_body ) as string_format
130
232
 
131
- class_inner_definitions = definition_body / definition_line / class_mapping_section / derive_definition / string_format_definition / attribute_comma_definition / ( !( "attribute" ws ) attribute_definition ) / keyword_attribute_definition / comment_definition / comment_multiline_definition
233
+ class_inner_definitions = visibility_declaration / abstract_declaration / definition_body / definition_line / class_mapping_section / derive_definition / string_format_definition / ( operation_definition ) as operations / attribute_comma_definition / ( !( kw_attribute_sp ) attribute_definition ) / keyword_attribute_definition / comment_definition / comment_multiline_definition
132
234
  class_inner_definition = class_inner_definitions [ whitespace ]
133
- class_body = [ ws ] "{" [ whitespace ] ( *( class_inner_definition ) ) as members "}"
134
- parent_class = [ ws ] "<" [ ws ] ( class_name_chars ) as parent_class
135
- class_definition = class_modifier [ whitespace ] "class" ws class_name as name [ parent_class ] [ ws ] [ attribute_stereotype ] [ class_body ]
235
+ class_body = "{" [ whitespace ] ( *( class_inner_definition ) ) as members "}"
236
+ parent_class = "<" ( class_name_chars ) as parent_class
237
+ class_definition = [ whitespace ] [ element_visibility ] [ whitespace ] class_modifier [ whitespace ] kw_class_sp class_name as name [ parent_class ] [ attribute_stereotype ] [ class_body ]
136
238
 
137
239
  # Balanced-brace definition text: nested "{...}" and "\"-escapes allowed
138
240
  definition_content = *( ( "\\" any_char ) / ( "{" definition_content "}" ) / ( !"}" any_char ) )
139
- definition_body = [ ws ] "definition" [ whitespace ] "{" ( definition_content ) as definition "}"
140
- definition_line = ( [ ws ] "definition" ws ( dq_string / sq_string ) ) as definition [ whitespace ]
241
+ definition_body = "definition" [ whitespace ] "{" ( definition_content ) as definition "}"
242
+ definition_line = ( kw_definition_sp ( dq_string / sq_string ) ) as definition [ whitespace ]
141
243
 
142
- member_field_line = [ ws ] attribute_name [ ws ] ( 1*nonspace ) as member_field_type [ whitespace ]
244
+ member_field_line = attribute_name ( 1*nonspace ) as member_field_type [ whitespace ]
143
245
  member_fields_definition = ( [ whitespace ] "member_fields" [ whitespace ] "{" [ whitespace ] ( *( member_field_line ) ) as member_fields [ whitespace ] "}" ) as member_fields_decl
144
- attribute_comma_definition = ( [ whitespace ] "attribute" ws attribute_name "," [ ws ] ( 1*typechar ) as type [ ws ] [ "{" [ whitespace ] keyword_attribute_options [ whitespace ] "}" ] [ whitespace ] ) as attributes
145
- enum_member_with_payload = ( [ whitespace ] attribute_name [ ws ] ( class_body / keyword_attribute_body ) ) as attributes
246
+ attribute_comma_definition = ( [ whitespace ] kw_attribute_sp attribute_name "," ( type_token ) as type [ "{" [ whitespace ] keyword_attribute_options [ whitespace ] "}" ] [ whitespace ] ) as attributes
247
+ enum_member_with_payload = ( [ whitespace ] attribute_name ( class_body / keyword_attribute_body ) ) as attributes
146
248
 
147
249
  # ---- Canonical value construct (RS 3001 par. Value / par. Enums) ----
148
250
  # `value <name> { block }` is the canonical enum member; the bare
@@ -151,81 +253,81 @@ grammar Lml version "1.0.0" {
151
253
  value_name = quoted_string / map_name
152
254
  value_inner_definition = ( definition_body / definition_line / instance / attribute_line / comment_definition / comment_multiline_definition ) [ whitespace ]
153
255
  value_body = "{" [ whitespace ] ( *( value_inner_definition ) ) as members "}"
154
- value_definition = ( [ whitespace ] "value" ws ( value_name ) as value_name [ ws ] [ value_body ] [ whitespace ] ) as value_decl
256
+ value_definition = ( [ whitespace ] kw_value_sp ( value_name ) as value_name [ value_body ] [ whitespace ] ) as value_decl
155
257
 
156
- from_table_definition = ( [ whitespace ] "from_table" [ ws ] ( map_name ) as from_table_name [ ws ] [ "{" [ whitespace ] *( "artifact" [ ws ] ( map_name ) as artifact_table [ whitespace ] ) "}" ] [ whitespace ] ) as from_table_decl
258
+ from_table_definition = ( [ whitespace ] "from_table" ( map_name ) as from_table_name [ "{" [ whitespace ] *( kw_artifact_sp ( map_name ) as artifact_table [ whitespace ] ) "}" ] [ whitespace ] ) as from_table_decl
157
259
 
158
- enum_values_shorthand = ( [ whitespace ] "values" ws value_set_body [ whitespace ] ) as value_set_decl
159
- enum_inner_definitions = definition_body / definition_line / member_fields_definition / from_table_definition / enum_values_shorthand / value_definition / enum_member_with_payload / attribute_comma_definition / attribute_definition / comment_definition / comment_multiline_definition
260
+ enum_values_shorthand = ( [ whitespace ] kw_values_sp value_set_body [ whitespace ] ) as value_set_decl
261
+ enum_inner_definitions = visibility_declaration / abstract_declaration / definition_body / definition_line / member_fields_definition / from_table_definition / enum_values_shorthand / value_definition / enum_member_with_payload / attribute_comma_definition / attribute_definition / comment_definition / comment_multiline_definition
160
262
  enum_inner_definition = enum_inner_definitions [ whitespace ]
161
- enum_body = [ ws ] "{" [ whitespace ] ( *( enum_inner_definition ) ) as members "}"
162
- enum_definition = [ whitespace ] "enum" ws [ quotes ] class_name as name [ quotes ] [ attribute_stereotype ] [ enum_body ]
163
- data_type_body = [ ws ] "{" [ whitespace ] ( *( enum_inner_definition ) ) as members "}"
164
- data_type_definition = [ whitespace ] "data_type" ws [ quotes ] class_name as name [ quotes ] [ attribute_stereotype ] [ data_type_body ]
165
- primitive_definition = [ whitespace ] "primitive" ws [ quotes ] class_name as name [ quotes ]
166
-
167
- package_body = [ ws ] "{" [ whitespace ] ( *( diagram_inner_definition ) ) as members "}"
168
- package_definition = ( [ whitespace ] "package" ws class_name as name [ ws ] package_body [ whitespace ] ) as packages
169
- diagram_inner_definitions = namespace_definition / mapping_definition / package_definition / title_definition / caption_definition / fontname_definition / ( class_definition ) as classes / ( enum_definition ) as enums / ( primitive_definition ) as primitives / ( data_type_definition ) as data_types / ( association_definition ) as associations / comment_definition / comment_multiline_definition
263
+ enum_body = "{" [ whitespace ] ( *( enum_inner_definition ) ) as members "}"
264
+ enum_definition = [ whitespace ] [ element_visibility ] kw_enum_sp [ quotes ] class_name as name [ quotes ] [ attribute_stereotype ] [ enum_body ]
265
+ data_type_body = "{" [ whitespace ] ( *( enum_inner_definition ) ) as members "}"
266
+ data_type_definition = [ whitespace ] [ element_visibility ] kw_data_type_sp [ quotes ] class_name as name [ quotes ] [ attribute_stereotype ] [ data_type_body ]
267
+ primitive_definition = [ whitespace ] [ element_visibility ] kw_primitive_sp [ quotes ] class_name as name [ quotes ]
268
+
269
+ package_body = "{" [ whitespace ] ( *( diagram_inner_definition ) ) as members "}"
270
+ package_definition = ( [ whitespace ] [ element_visibility ] kw_package_sp class_name as name package_body [ whitespace ] ) as packages
271
+ diagram_inner_definitions = namespace_definition / mapping_definition / ( class_definition ) as classes / ( enum_definition ) as enums / ( primitive_definition ) as primitives / ( data_type_definition ) as data_types / package_definition / title_definition / caption_definition / fontname_definition / ( association_definition ) as associations / comment_definition / comment_multiline_definition
170
272
  diagram_inner_definition = diagram_inner_definitions [ whitespace ]
171
- diagram_body = [ ws ] "{" [ whitespace ] ( *( diagram_inner_definition ) ) as members "}"
172
- diagram_definition = [ whitespace ] "diagram" [ ws ] class_name as name diagram_body [ whitespace ]
273
+ diagram_body = "{" [ whitespace ] ( *( diagram_inner_definition ) ) as members "}"
274
+ diagram_definition = [ whitespace ] "diagram" class_name as name diagram_body [ whitespace ]
173
275
  diagram_definitions = diagram_definition [ whitespace ]
174
276
  model_definitions = ( 1*( diagram_inner_definition ) ) as members
175
277
 
176
278
  view_only_definitions = ( view_import ) as view_imports / show_directive / hide_directive
177
279
  view_inner_definitions = view_only_definitions / diagram_inner_definitions
178
280
  view_inner_definition = view_inner_definitions [ whitespace ]
179
- view_body = [ ws ] "{" [ whitespace ] ( *( view_inner_definition ) ) as members "}"
180
- view_definition = [ whitespace ] "view" [ ws ] class_name as name view_body [ whitespace ]
281
+ view_body = "{" [ whitespace ] ( *( view_inner_definition ) ) as members "}"
282
+ view_definition = [ whitespace ] "view" class_name as name view_body [ whitespace ]
181
283
  view_definitions = view_definition [ whitespace ]
182
284
 
183
- title_text = ( %x22 ( 1*( !%x22 any_char ) ) as title %x22 ) / ( %x27 ( 1*( !%x27 any_char ) ) as title %x27 ) / ( 1*( %x41-5A / %x61-7A / %x30-39 / "_" / "-" / " " / "," / "." / ":" / ";" / "(" / ")" / "+" / "'" / "/" ) ) as title
184
- title_definition = [ whitespace ] "title" ws title_text
185
- caption_text = ( %x22 ( 1*( !%x22 any_char ) ) as caption %x22 ) / ( %x27 ( 1*( !%x27 any_char ) ) as caption %x27 ) / ( 1*( %x41-5A / %x61-7A / %x30-39 / "_" / "-" / " " / "," / "." / ":" / ";" / "(" / ")" / "+" / "'" / "/" ) ) as caption
186
- caption_definition = [ whitespace ] "caption" ws caption_text
285
+ atomic title_text = ( %x22 ( 1*( !%x22 ANY ) ) as title %x22 ) / ( %x27 ( 1*( !%x27 ANY ) ) as title %x27 ) / ( 1*( %x41-5A / %x61-7A / %x30-39 / "_" / "-" / " " / "," / "." / ":" / ";" / "(" / ")" / "+" / "'" / "/" ) ) as title
286
+ title_definition = [ whitespace ] kw_title_sp title_text
287
+ atomic caption_text = ( %x22 ( 1*( !%x22 ANY ) ) as caption %x22 ) / ( %x27 ( 1*( !%x27 ANY ) ) as caption %x27 ) / ( 1*( %x41-5A / %x61-7A / %x30-39 / "_" / "-" / " " / "," / "." / ":" / ";" / "(" / ")" / "+" / "'" / "/" ) ) as caption
288
+ caption_definition = [ whitespace ] kw_caption_sp caption_text
187
289
  fontname_text = [ quotes ] ( 1*( %x41-5A / %x61-7A / %x30-39 / "_" / "-" / " " ) ) as fontname [ quotes ]
188
- fontname_definition = [ whitespace ] "fontname" ws fontname_text
290
+ fontname_definition = [ whitespace ] kw_fontname_sp fontname_text
189
291
 
190
292
  # ---- Views ----
191
293
  # `import` here carries a required trailing space (word boundary)
192
- view_import = [ whitespace ] "import" ws ( dq_path / sq_path ) [ whitespace ]
294
+ view_import = [ whitespace ] kw_import_sp ( dq_path / sq_path ) [ whitespace ]
193
295
  entity_name_atom = ( 1*( %x41-5A / %x61-7A / %x30-39 / "_" / "." / ":" / "-" ) ) as entity_name
194
- entity_name_list = entity_name_atom *( [ ws ] "," [ ws ] entity_name_atom )
195
- show_directive = [ whitespace ] "show" ws ( entity_name_list ) as show_list [ whitespace ]
196
- hide_directive = [ whitespace ] "hide" ws ( entity_name_list ) as hide_list [ whitespace ]
296
+ entity_name_list = entity_name_atom *( "," entity_name_atom )
297
+ show_directive = [ whitespace ] kw_show_sp ( entity_name_list ) as show_list [ whitespace ]
298
+ hide_directive = [ whitespace ] kw_hide_sp ( entity_name_list ) as hide_list [ whitespace ]
197
299
 
198
300
  # ---- Requires ----
199
- require_stmt = [ whitespace ] "require" ws quoted_string as require [ whitespace ]
301
+ require_stmt = [ whitespace ] kw_require_sp quoted_string as require [ whitespace ]
200
302
  require_block = ( *( require_stmt [ whitespace ] ) ) as requires
201
303
 
202
304
  # ---- Instances ----
203
305
  instance = instance_named / keyword_instance / class_instance
204
- instance_named = ( [ whitespace ] "instance" ws quoted_string as name [ ws ] [ ( namespaced_identifier ) as instance_type ] [ ws ] "{" [ whitespace ] ( ( [ ws ] instance ) / attributes ) "}" [ whitespace ] ) as instance
205
- isa_definition = [ whitespace ] "isa" ws ( namespaced_identifier ) as isa [ whitespace ]
206
- keyword_instance = ( [ whitespace ] "instance" [ ws ( namespaced_identifier ) as instance_type ] [ ws ] "{" [ whitespace ] [ isa_definition ] ( ( [ ws ] instance ) / attributes ) "}" ) as instance [ whitespace ]
306
+ instance_named = ( [ whitespace ] kw_instance_sp quoted_string as name [ ( namespaced_identifier ) as instance_type ] "{" [ whitespace ] ( ( instance ) / attributes ) "}" [ whitespace ] ) as instance
307
+ isa_definition = [ whitespace ] kw_isa_sp ( namespaced_identifier ) as isa [ whitespace ]
308
+ keyword_instance = ( [ whitespace ] "instance" [ ( namespaced_identifier ) as instance_type ] "{" [ whitespace ] [ isa_definition ] ( ( instance ) / attributes ) "}" ) as instance [ whitespace ]
207
309
  class_instance = ( variable as instance_type [ whitespace ] quoted_string as name [ whitespace ] [ [ whitespace ] "extends" [ whitespace ] quoted_string as parent [ whitespace ] ] "{" [ whitespace ] [ lml_instance_body ] "}" ) as instance [ whitespace ]
208
310
  lml_instance_body = lml_instance_members [ whitespace ]
209
311
  instance_template = [ whitespace ] "template" [ whitespace ] "{" [ whitespace ] attributes [ whitespace ] "}"
210
312
  lml_instance_members = ( instance_template ) as template / attributes
211
- models = [ whitespace ] "models" [ whitespace ] variable as name [ whitespace ] "{" ( *( ( ( string_format_definition ) as default_string_formats ) / model_body ) ) as members "}" [ whitespace ]
313
+ models = [ whitespace ] "models" [ whitespace ] variable as name [ version_declaration ] [ whitespace ] "{" ( *( ( ( string_format_definition ) as default_string_formats ) / model_body ) ) as members "}" [ whitespace ]
212
314
  model_body = diagram_inner_definition
213
315
 
214
316
  # ---- Data structures ----
215
- attribute_value = instance / list / key_value_map / data_value / 1*( !%x0A any_char )
317
+ attribute_value = instance / list / key_value_map / data_value / 1*( !%x0A ANY )
216
318
  list_item = instance / data_value
217
- list = "[" [ whitespace ] ( *( list_item [ ws ] [ "," ] [ whitespace ] ) ) as list [ whitespace ] "]"
218
- collection = [ whitespace ] "collection" ws quoted_string as name [ ws ] "{" [ whitespace ] [ includes ] [ whitespace ] [ validation ] [ whitespace ] "}" [ whitespace ]
219
- includes = [ whitespace ] "includes" [ ws ] ( list ) as includes
220
- validation = [ whitespace ] "validation" [ ws ] "{" [ whitespace ] ( *( condition ) ) as validations "}"
221
- condition = [ whitespace ] "condition" ws quoted_string as condition [ whitespace ]
222
- import = [ whitespace ] "import" [ ws ] "{" [ whitespace ] ( *( import_definition ) ) as imports "}" [ whitespace ]
223
- import_definition = ( 1*nonspace ) as format_type [ ws ] quoted_string as file [ whitespace ] "{" [ whitespace ] attributes [ whitespace ] "}" [ whitespace ]
224
- instances = [ whitespace ] "instances" [ ws ] [ ( quoted_string / variable ) as name ] [ whitespace ] "{" [ whitespace ] [ instances_body ] "}" [ whitespace ]
319
+ list = "[" [ whitespace ] ( *( list_item [ "," ] [ whitespace ] ) ) as list [ whitespace ] "]"
320
+ collection = [ whitespace ] kw_collection_sp quoted_string as name "{" [ whitespace ] [ includes ] [ whitespace ] [ validation ] [ whitespace ] "}" [ whitespace ]
321
+ includes = [ whitespace ] "includes" ( list ) as includes
322
+ validation = [ whitespace ] "validation" "{" [ whitespace ] ( *( condition ) ) as validations "}"
323
+ condition = [ whitespace ] kw_condition_sp quoted_string as condition [ whitespace ]
324
+ import = [ whitespace ] "import" "{" [ whitespace ] ( *( import_definition ) ) as imports "}" [ whitespace ]
325
+ import_definition = ( 1*nonspace ) as format_type quoted_string as file [ whitespace ] "{" [ whitespace ] attributes [ whitespace ] "}" [ whitespace ]
326
+ instances = [ whitespace ] "instances" [ ( quoted_string / variable ) as name ] [ whitespace ] "{" [ whitespace ] [ instances_body ] "}" [ whitespace ]
225
327
  instances_body = ( *( instances_member [ whitespace ] ) ) as instances
226
328
  instances_member = import / ( collection ) as collections / export / instance
227
329
  export = [ whitespace ] "export" [ whitespace ] "{" [ whitespace ] ( *( export_format [ whitespace ] ) ) as exports "}" [ whitespace ]
228
- export_format = [ whitespace ] "format" ws variable as format_type [ whitespace ] "{" [ whitespace ] attributes "}" [ whitespace ]
330
+ export_format = [ whitespace ] kw_format_sp variable as format_type [ whitespace ] "{" [ whitespace ] attributes "}" [ whitespace ]
229
331
 
230
332
  # ---- Root (RS 3001: a document interleaves declarations) ----
231
333
  diagram = [ require_block ] ( *( top_level_definition ) ) as members
@@ -234,3 +336,16 @@ grammar Lml version "1.0.0" {
234
336
  }
235
337
 
236
338
  entry diagram: diagram
339
+
340
+ # Compile-time contract tests for the trivia/skip behavior (run by
341
+ # `rake parg` and any in-memory compile; a failure fails the build).
342
+ test diagram {
343
+ accept "// leading\nclass Ceramic { }\n"
344
+ accept "class /* inline */ Ceramic { }\n"
345
+ accept "class Ceramic { } // trailing"
346
+ accept "instances {\n P \"p\" {\n url = \"http://a//b\"\n }\n}\n"
347
+ reject "classCeramic Foo { }"
348
+ reject "abstractclass Ceramic { }"
349
+ reject "class\tCeramic { }"
350
+ reject "class Ceramic { /* unterminated"
351
+ }
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Layout
5
+ # In-process layout engine backed by elkrb (pure-Ruby ELK). The input
6
+ # is an already-built Elkrb::Graph; rendering produces standalone SVG.
7
+ class ElkEngine < Engine
8
+ def render(_type)
9
+ require 'elkrb'
10
+ Elkrb::Layout::LayoutEngine.layout(@input, algorithm: 'layered')
11
+ ElkSvgRenderer.new.render(@input)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Layout
5
+ # Builds an Elkrb::Graph: a node per classifier (name, definition,
6
+ # members); an edge per association and parent specializer.
7
+ class ElkGraphBuilder
8
+ CHAR_WIDTH = 7.2
9
+ LINE_HEIGHT = 16.0
10
+ PADDING = 14.0
11
+ MIN_WIDTH = 120.0
12
+ COLLECTIONS = %i[classes enums primitives data_types].freeze
13
+ def build(document)
14
+ graph = elkrb_graph
15
+ add_entities(graph, document)
16
+ add_association_edges(graph, document)
17
+ add_parent_edges(graph, document)
18
+ graph
19
+ end
20
+
21
+ private
22
+
23
+ def elkrb_graph
24
+ require 'elkrb'
25
+ Elkrb::Graph::Graph.new(id: 'diagram')
26
+ rescue LoadError
27
+ raise Error, 'The elkrb gem is required for ELK layout: add `gem "elkrb"` to your Gemfile'
28
+ end
29
+
30
+ def entities(document)
31
+ ([document] + Array(document.packages)).flat_map { |scope| scope_entities(scope) }.flatten.compact
32
+ end
33
+
34
+ def scope_entities(scope)
35
+ COLLECTIONS.filter_map do |collection|
36
+ next unless scope.respond_to?(collection)
37
+
38
+ scope.public_send(collection)
39
+ end
40
+ end
41
+
42
+ def node_for(entity)
43
+ lines = label_lines(entity)
44
+ Elkrb::Graph::Node.new(
45
+ id: entity_id_for(entity.name),
46
+ width: node_width(lines),
47
+ height: PADDING + lines.size * LINE_HEIGHT,
48
+ labels: lines.each_with_index.map do |line, i|
49
+ Elkrb::Graph::Label.new(text: line, x: 0, y: i * LINE_HEIGHT)
50
+ end
51
+ )
52
+ end
53
+
54
+ def label_lines(entity)
55
+ lines = [entity.name.to_s]
56
+ if entity.respond_to?(:definition) && !entity.definition.to_s.empty?
57
+ lines << entity.definition.to_s.gsub("\n", ' ')
58
+ end
59
+ Array(entity.attributes).each do |attr|
60
+ lines << attr_line(attr)
61
+ end
62
+ lines
63
+ end
64
+
65
+ def attr_line(attr)
66
+ line = "+ #{attr.name}"
67
+ line += ": #{attr.type}" if attr.type && !attr.type.to_s.empty?
68
+ line
69
+ end
70
+
71
+ def node_width(lines)
72
+ widest = lines.map(&:length).max || 0
73
+ [widest * CHAR_WIDTH + PADDING, MIN_WIDTH].max
74
+ end
75
+
76
+ def entity_id(entity)
77
+ entity.name.to_s.gsub(/[^0-9a-zA-Z_]/, '_')
78
+ end
79
+
80
+ def associations(document)
81
+ Array(document.associations)
82
+ end
83
+
84
+ def parent_edges(document)
85
+ Array(document.classes).filter_map do |klass|
86
+ next unless klass.respond_to?(:parent_class) && klass.parent_class
87
+
88
+ { owner: klass.parent_class, member: klass.name.to_s,
89
+ member_end_type: 'generalization', name: nil }
90
+ end
91
+ end
92
+
93
+ def add_entities(graph, document)
94
+ entities(document).each { |entity| graph.children << node_for(entity) }
95
+ end
96
+
97
+ def add_association_edges(graph, document)
98
+ associations(document).each { |assoc| graph.edges << edge_for(association_spec(assoc)) }
99
+ end
100
+
101
+ def add_parent_edges(graph, document)
102
+ parent_edges(document).each { |edge| graph.edges << edge_for(edge) }
103
+ end
104
+
105
+ def edge_for(spec)
106
+ edge = elkrb_edge(spec)
107
+ edge.properties = { 'member_end_type' => spec[:member_end_type].to_s }
108
+ edge.labels = [Elkrb::Graph::Label.new(text: spec[:name].to_s)] if spec[:name]
109
+ edge
110
+ end
111
+
112
+ def elkrb_edge(spec)
113
+ require 'elkrb'
114
+ Elkrb::Graph::Edge.new(
115
+ id: "edge_#{spec[:owner]}_#{spec[:member]}",
116
+ sources: [entity_id_for(spec[:owner])],
117
+ targets: [entity_id_for(spec[:member])]
118
+ )
119
+ end
120
+
121
+ def association_spec(assoc)
122
+ { owner: assoc.owner_end.to_s, member: assoc.member_end.to_s,
123
+ member_end_type: assoc.member_end_type.to_s, name: assoc.name }
124
+ end
125
+
126
+ def entity_id_for(name) = name.to_s.gsub(/[^0-9a-zA-Z_]/, '_')
127
+ end
128
+ end
129
+ end