lutaml-lml 0.2.1 → 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.
- checksums.yaml +4 -4
- data/lib/lutaml/lml/cli.rb +76 -17
- data/lib/lutaml/lml/data_processor/value_processing.rb +6 -1
- data/lib/lutaml/lml/format/adapter/mapping.rb +12 -0
- data/lib/lutaml/lml/format/adapter/standard_adapter.rb +24 -8
- data/lib/lutaml/lml/formatter/elk.rb +5 -2
- data/lib/lutaml/lml/formatter/svg_pdf/path_parser.rb +134 -0
- data/lib/lutaml/lml/formatter/svg_pdf.rb +281 -0
- data/lib/lutaml/lml/formatter.rb +1 -0
- data/lib/lutaml/lml/grammar/lml.artifact.json +2505 -2234
- data/lib/lutaml/lml/grammar/lml.parg +25 -9
- data/lib/lutaml/lml/model_compiler/enums.rb +172 -0
- data/lib/lutaml/lml/model_compiler/hydration.rb +123 -0
- data/lib/lutaml/lml/model_compiler/serialization_mappings.rb +162 -0
- data/lib/lutaml/lml/model_compiler/string_formats.rb +42 -0
- data/lib/lutaml/lml/model_compiler/validation.rb +65 -0
- data/lib/lutaml/lml/model_compiler.rb +46 -474
- data/lib/lutaml/lml/transform.rb +53 -5
- data/lib/lutaml/lml/validator.rb +2 -1
- data/lib/lutaml/lml/version.rb +1 -1
- data/lib/lutaml/lml.rb +18 -3
- metadata +9 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c44e08a243084f376e006dfa845a878267d5867d1c07e9cb2f957cadfdf593af
|
|
4
|
+
data.tar.gz: 56b169b4f31489f51713071e48002ec2b0babc8f6b510aa6660e7b02976b0f93
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 12590d775d46b14bbefe7733610b6b60c08ddaa169e6aa3e4010b13979e565d36bde270adffc5e7ef6cdb37c9fbcadedb16d66b0bbc1e6404d74c46da5b2c1ec
|
|
7
|
+
data.tar.gz: 1dcf78fc5244594d03992cdb8dd4b2e286c0069741b3985bad2667ea1752eab19534aa16878e9e2c80ac3ab4b0c5c2381756634590a99df1070e72a0e6b7bf83
|
data/lib/lutaml/lml/cli.rb
CHANGED
|
@@ -30,7 +30,7 @@ module Lutaml
|
|
|
30
30
|
'exp' => lambda { |path|
|
|
31
31
|
require 'lutaml/express'
|
|
32
32
|
Lutaml::Express::Parsers::Exp.parse(File.new(path))
|
|
33
|
-
}
|
|
33
|
+
},
|
|
34
34
|
}.freeze
|
|
35
35
|
|
|
36
36
|
FORMATTER_ATTR_TARGETS = %i[graph edge node].freeze
|
|
@@ -115,36 +115,95 @@ module Lutaml
|
|
|
115
115
|
Parse an LML file containing model definitions and compile them
|
|
116
116
|
into Ruby classes that can be used to instantiate and validate data.
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
With --namespace, classes are registered as constants in the given
|
|
119
|
+
module (created if absent).
|
|
120
|
+
|
|
121
|
+
With --schema, emits a JSON Schema or YAML Schema document for every
|
|
122
|
+
compiled model instead of the compile listing. Schema output requires
|
|
123
|
+
named classes, so --namespace defaults to Lutaml::Lml::Generated when omitted.
|
|
121
124
|
|
|
122
125
|
Examples:
|
|
123
126
|
lutaml lml compile models.lml
|
|
124
127
|
|
|
125
128
|
lutaml lml compile models.lml --namespace MyModels
|
|
129
|
+
|
|
130
|
+
lutaml lml compile models.lml --schema json -o schemas/
|
|
131
|
+
|
|
132
|
+
lutaml lml compile models.lml --schema yaml -o widget.schema.yaml
|
|
126
133
|
DESC
|
|
127
134
|
method_option :namespace, type: :string, aliases: '-n',
|
|
128
|
-
|
|
135
|
+
desc: 'Register compiled classes in a module (created if absent)'
|
|
136
|
+
method_option :schema, type: :string, enum: %w[json yaml],
|
|
137
|
+
desc: 'Emit JSON Schema or YAML Schema for the compiled models'
|
|
138
|
+
method_option :output, type: :string, aliases: '-o',
|
|
139
|
+
desc: 'Schema output path (file for one model, directory for many)'
|
|
129
140
|
def compile(path)
|
|
130
141
|
raise Thor::Error, "File does not exist: #{path}" unless File.exist?(path)
|
|
131
142
|
|
|
132
|
-
ns = options[:namespace]
|
|
133
|
-
result = Lutaml::Lml::ModelCompiler.new(namespace: ns).compile(
|
|
143
|
+
ns = options[:namespace] || (::Lutaml::Lml::ModelCompiler::DEFAULT_NAMESPACE if options[:schema])
|
|
144
|
+
result = File.open(path) { |file| Lutaml::Lml::ModelCompiler.new(namespace: ns).compile(file) }
|
|
145
|
+
return emit_schemas(result) if options[:schema]
|
|
134
146
|
|
|
135
|
-
result
|
|
136
|
-
say " compiled: #{name}", :green
|
|
137
|
-
end
|
|
138
|
-
say "\n#{result.size} class(es) compiled", :green
|
|
147
|
+
report_compilation(result)
|
|
139
148
|
end
|
|
140
149
|
|
|
141
150
|
no_commands do
|
|
142
|
-
def
|
|
143
|
-
|
|
144
|
-
|
|
151
|
+
def report_compilation(result)
|
|
152
|
+
result.each_key do |name|
|
|
153
|
+
say " compiled: #{name}", :green
|
|
154
|
+
end
|
|
155
|
+
say "\n#{result.size} class(es) compiled", :green
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def emit_schemas(result)
|
|
159
|
+
require 'lutaml/model/schema'
|
|
160
|
+
if options[:output] && Pathname.new(options[:output]).directory?
|
|
161
|
+
generator = "to_#{options[:schema]}"
|
|
162
|
+
schemas = result.transform_values do |klass|
|
|
163
|
+
Lutaml::Model::Schema.public_send(generator, klass, pretty: true)
|
|
164
|
+
end
|
|
165
|
+
write_schemas(schemas)
|
|
166
|
+
else
|
|
167
|
+
# One multi-root document covers every compiled model; each
|
|
168
|
+
# root stays addressable under $defs by class name.
|
|
169
|
+
schema = generate_multi_root_schema(result)
|
|
170
|
+
return puts schema unless options[:output]
|
|
171
|
+
|
|
172
|
+
File.write(options[:output], schema)
|
|
173
|
+
say "Generated: #{options[:output]}", :green
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def generate_multi_root_schema(result)
|
|
178
|
+
klass = options[:schema] == 'yaml' ? Lutaml::Model::Schema::YamlSchema : Lutaml::Model::Schema::JsonSchema
|
|
179
|
+
klass.generate_many(result.values, pretty: true)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def write_schemas(schemas)
|
|
183
|
+
target = Pathname.new(options[:output])
|
|
184
|
+
if schemas.size > 1
|
|
185
|
+
unless target.directory?
|
|
186
|
+
raise Thor::Error,
|
|
187
|
+
'Output path must be a directory if multiple schemas ' \
|
|
188
|
+
'are generated'
|
|
189
|
+
end
|
|
190
|
+
write_schema_files(target, schemas)
|
|
191
|
+
else
|
|
192
|
+
target.write(schemas.values.first)
|
|
193
|
+
say "Generated: #{target}", :green
|
|
145
194
|
end
|
|
146
|
-
|
|
147
|
-
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def write_schema_files(dir, schemas)
|
|
198
|
+
schemas.each do |name, schema|
|
|
199
|
+
file = dir.join("#{schema_file_stem(name)}.#{options[:schema]}")
|
|
200
|
+
file.write(schema)
|
|
201
|
+
say "Generated: #{file}", :green
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def schema_file_stem(name)
|
|
206
|
+
name.split('::').last.gsub(/[^a-zA-Z0-9]+/, '_').downcase
|
|
148
207
|
end
|
|
149
208
|
|
|
150
209
|
def assert_input_paths(paths)
|
|
@@ -182,7 +241,7 @@ module Lutaml
|
|
|
182
241
|
def write_output(input_path, result)
|
|
183
242
|
target = resolve_output_path(input_path)
|
|
184
243
|
if target.is_a?(Pathname)
|
|
185
|
-
target.open('
|
|
244
|
+
target.open('wb+') { |f| f.write(result) }
|
|
186
245
|
say "Generated: #{target}", :green
|
|
187
246
|
else
|
|
188
247
|
target.puts(result)
|
|
@@ -4,13 +4,14 @@ module Lutaml
|
|
|
4
4
|
module Lml
|
|
5
5
|
class DataProcessor
|
|
6
6
|
module ValueProcessing
|
|
7
|
-
VALUE_TYPE_KEYS = %i[instance list string boolean key_value_map number float condition require].freeze
|
|
7
|
+
VALUE_TYPE_KEYS = %i[instance list string boolean null key_value_map number float condition require].freeze
|
|
8
8
|
|
|
9
9
|
VALUE_TYPE_HANDLERS = {
|
|
10
10
|
instance: :handle_instance_value,
|
|
11
11
|
list: :handle_list_value,
|
|
12
12
|
string: :handle_string_value,
|
|
13
13
|
boolean: :handle_boolean_value,
|
|
14
|
+
null: :handle_null_value,
|
|
14
15
|
key_value_map: :handle_key_value_map,
|
|
15
16
|
number: :handle_number_value,
|
|
16
17
|
float: :handle_float_value,
|
|
@@ -50,6 +51,10 @@ module Lutaml
|
|
|
50
51
|
['Boolean', value[:boolean] == 'true']
|
|
51
52
|
end
|
|
52
53
|
|
|
54
|
+
def handle_null_value(_key, _value)
|
|
55
|
+
['Null', nil]
|
|
56
|
+
end
|
|
57
|
+
|
|
53
58
|
def handle_key_value_map(_key, value)
|
|
54
59
|
process_key_value_map(value[:key_value_map])
|
|
55
60
|
end
|
|
@@ -12,6 +12,18 @@ module Lutaml
|
|
|
12
12
|
def dup_instance
|
|
13
13
|
self.class.new
|
|
14
14
|
end
|
|
15
|
+
|
|
16
|
+
# RS 3001 §String values: a present value — including a
|
|
17
|
+
# zero-length string — is distinct from an absent one, so the
|
|
18
|
+
# lml format emits empty values. Nil stays omitted: absence is
|
|
19
|
+
# the unassigned line, not a rendered null.
|
|
20
|
+
def map(name = nil, to: nil, render_empty: true, **options, &block)
|
|
21
|
+
super(name, to: to, render_empty: render_empty, **options, &block)
|
|
22
|
+
end
|
|
23
|
+
# The parent's `alias map_element map` binds the alias to the
|
|
24
|
+
# parent method at definition time, so it bypasses the map
|
|
25
|
+
# override above; default_mappings goes through map_element.
|
|
26
|
+
alias map_element map
|
|
15
27
|
end
|
|
16
28
|
end
|
|
17
29
|
end
|
|
@@ -4,9 +4,21 @@ module Lutaml
|
|
|
4
4
|
module Lml
|
|
5
5
|
module Format
|
|
6
6
|
module Adapter
|
|
7
|
+
# Serializes instance data to and from LML instance syntax on
|
|
8
|
+
# behalf of the lutaml-model format registry (to_lml/from_lml on
|
|
9
|
+
# compiled classes).
|
|
7
10
|
class StandardAdapter < Document
|
|
8
11
|
TYPE_KEY = "__type__"
|
|
9
12
|
|
|
13
|
+
# The grammar's `word` rule is [A-Za-z0-9_] only (lml.parg) and
|
|
14
|
+
# `variable = quoted_string / 1*word` — a bare value is a run of
|
|
15
|
+
# word characters and nothing else. Anything else (a dash, a
|
|
16
|
+
# space, a digit-leading date...) commits the value parser to
|
|
17
|
+
# `number` or fails re-parsing, so it must be emitted quoted.
|
|
18
|
+
# This predicate is the single source of truth for the emit
|
|
19
|
+
# side; the parse side is pinned by the grammar itself.
|
|
20
|
+
BARE_WORD = /\A\w+\z/
|
|
21
|
+
|
|
10
22
|
def self.parse(data, _options = {})
|
|
11
23
|
return data if data.is_a?(Hash)
|
|
12
24
|
|
|
@@ -22,8 +34,6 @@ module Lutaml
|
|
|
22
34
|
"instance #{type_name} {\n#{body}\n}"
|
|
23
35
|
end
|
|
24
36
|
|
|
25
|
-
private
|
|
26
|
-
|
|
27
37
|
def self.instance_to_hash(instance)
|
|
28
38
|
return nil unless instance
|
|
29
39
|
|
|
@@ -41,20 +51,18 @@ module Lutaml
|
|
|
41
51
|
end
|
|
42
52
|
end
|
|
43
53
|
|
|
44
|
-
if instance.instance
|
|
45
|
-
hash.merge!(instance_to_hash(instance.instance))
|
|
46
|
-
end
|
|
54
|
+
hash.merge!(instance_to_hash(instance.instance)) if instance.instance
|
|
47
55
|
|
|
48
56
|
hash
|
|
49
57
|
end
|
|
50
58
|
|
|
51
59
|
def self.primitive_value(val)
|
|
52
60
|
case val
|
|
53
|
-
when TrueClass, FalseClass then val
|
|
54
|
-
when Integer, Float then val
|
|
61
|
+
when TrueClass, FalseClass, Integer, Float then val
|
|
55
62
|
else val.to_s
|
|
56
63
|
end
|
|
57
64
|
end
|
|
65
|
+
private_class_method :primitive_value
|
|
58
66
|
|
|
59
67
|
def hash_to_lml_body(hash, indent = 1)
|
|
60
68
|
lines = []
|
|
@@ -126,8 +134,16 @@ module Lutaml
|
|
|
126
134
|
|
|
127
135
|
def quote_value(val)
|
|
128
136
|
return val.to_s if val.is_a?(Numeric) || val.is_a?(TrueClass) || val.is_a?(FalseClass)
|
|
137
|
+
|
|
129
138
|
str = val.to_s
|
|
130
|
-
str.match?(
|
|
139
|
+
return str if str.match?(BARE_WORD)
|
|
140
|
+
|
|
141
|
+
# RS 3001 §String values: escape backslash and double quote;
|
|
142
|
+
# every value is representable in double quotes.
|
|
143
|
+
# gsub replacement strings re-interpret backslash pairs, so
|
|
144
|
+
# the backslash doubling uses the block form.
|
|
145
|
+
escaped = str.gsub('\\') { |bs| bs + bs }.gsub('"') { |q| '\\' + q }
|
|
146
|
+
"\"#{escaped}\""
|
|
131
147
|
end
|
|
132
148
|
end
|
|
133
149
|
end
|
|
@@ -4,9 +4,10 @@ module Lutaml
|
|
|
4
4
|
module Formatter
|
|
5
5
|
# ELK-layout diagram formatter: builds an Elkrb::Graph from the
|
|
6
6
|
# document, lays it out in-process (no external tools) and emits SVG,
|
|
7
|
-
#
|
|
7
|
+
# PS/EPS/EMF through vectory, or PDF through the pdfrb-backed
|
|
8
|
+
# SvgPdf renderer.
|
|
8
9
|
class Elk
|
|
9
|
-
VALID_TYPES = %i[svg ps eps emf].freeze
|
|
10
|
+
VALID_TYPES = %i[svg ps eps emf pdf].freeze
|
|
10
11
|
|
|
11
12
|
attr_reader :type
|
|
12
13
|
|
|
@@ -31,6 +32,8 @@ module Lutaml
|
|
|
31
32
|
private
|
|
32
33
|
|
|
33
34
|
def convert(svg)
|
|
35
|
+
return SvgPdf.new(svg).render if @type == :pdf
|
|
36
|
+
|
|
34
37
|
require 'vectory'
|
|
35
38
|
Vectory::Svg.new(svg).public_send(:"to_#{@type}").content
|
|
36
39
|
rescue LoadError
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Formatter
|
|
5
|
+
class SvgPdf
|
|
6
|
+
# Minimal SVG path data parser: absolute and relative M/m, L/l,
|
|
7
|
+
# H/h, V/v, C/c, Z/z — enough for ELK geometry and arrow shapes.
|
|
8
|
+
class PathParser
|
|
9
|
+
Point = Struct.new(:x, :y)
|
|
10
|
+
Line = Struct.new(:point) do
|
|
11
|
+
def last_point
|
|
12
|
+
point
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
Curve = Struct.new(:c1, :c2, :point) do
|
|
16
|
+
def last_point
|
|
17
|
+
point
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
Close = Struct.new(:last_point)
|
|
21
|
+
|
|
22
|
+
attr_reader :subpaths
|
|
23
|
+
|
|
24
|
+
def initialize(d, dx, dy)
|
|
25
|
+
@dx = dx
|
|
26
|
+
@dy = dy
|
|
27
|
+
parse(d)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def parse(d)
|
|
33
|
+
tokens = d.scan(/[MmLlHhVvCcZz]|-?\d*\.?\d+(?:[eE][-+]?\d+)?/)
|
|
34
|
+
current = Point.new(0.0, 0.0)
|
|
35
|
+
start = current
|
|
36
|
+
@subpaths = [[]]
|
|
37
|
+
i = 0
|
|
38
|
+
while i < tokens.size
|
|
39
|
+
cmd = tokens[i]
|
|
40
|
+
i += 1
|
|
41
|
+
case cmd
|
|
42
|
+
when 'Z', 'z'
|
|
43
|
+
@subpaths.last << Close.new(start)
|
|
44
|
+
current = start.dup
|
|
45
|
+
when 'M', 'm'
|
|
46
|
+
current, start = move_to(tokens, i, current, cmd == 'm')
|
|
47
|
+
i += 2
|
|
48
|
+
@subpaths << [] if cmd == 'm'
|
|
49
|
+
when 'L', 'l'
|
|
50
|
+
current = line_to(tokens, i, current, cmd == 'l')
|
|
51
|
+
i += 2
|
|
52
|
+
when 'H', 'h'
|
|
53
|
+
current = h_line(tokens, i, current, cmd == 'h')
|
|
54
|
+
i += 1
|
|
55
|
+
when 'V', 'v'
|
|
56
|
+
current = v_line(tokens, i, current, cmd == 'v')
|
|
57
|
+
i += 1
|
|
58
|
+
when 'C', 'c'
|
|
59
|
+
current = curve_to(tokens, i, current, cmd == 'c')
|
|
60
|
+
i += 6
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
finish
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def move_to(tokens, i, current, relative)
|
|
67
|
+
x = num(tokens, i)
|
|
68
|
+
y = num(tokens, i + 1)
|
|
69
|
+
point = relative ? Point.new(current.x + x, current.y + y) : Point.new(x, y)
|
|
70
|
+
[point, point.dup]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def line_to(tokens, i, current, relative)
|
|
74
|
+
x = num(tokens, i)
|
|
75
|
+
y = num(tokens, i + 1)
|
|
76
|
+
point = relative ? Point.new(current.x + x, current.y + y) : Point.new(x, y)
|
|
77
|
+
@subpaths.last << Line.new(point.dup)
|
|
78
|
+
point
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def h_line(tokens, i, current, relative)
|
|
82
|
+
x = num(tokens, i) + (relative ? current.x : 0)
|
|
83
|
+
point = Point.new(x, current.y)
|
|
84
|
+
@subpaths.last << Line.new(point.dup)
|
|
85
|
+
point
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def v_line(tokens, i, current, relative)
|
|
89
|
+
y = num(tokens, i) + (relative ? current.y : 0)
|
|
90
|
+
point = Point.new(current.x, y)
|
|
91
|
+
@subpaths.last << Line.new(point.dup)
|
|
92
|
+
point
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def curve_to(tokens, i, current, relative)
|
|
96
|
+
c1 = Point.new(num(tokens, i), num(tokens, i + 1))
|
|
97
|
+
c2 = Point.new(num(tokens, i + 2), num(tokens, i + 3))
|
|
98
|
+
to = Point.new(num(tokens, i + 4), num(tokens, i + 5))
|
|
99
|
+
if relative
|
|
100
|
+
c1 = Point.new(c1.x + current.x, c1.y + current.y)
|
|
101
|
+
c2 = Point.new(c2.x + current.x, c2.y + current.y)
|
|
102
|
+
to = Point.new(to.x + current.x, to.y + current.y)
|
|
103
|
+
end
|
|
104
|
+
@subpaths.last << Curve.new(c1, c2, to)
|
|
105
|
+
to
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def num(tokens, i)
|
|
109
|
+
tokens[i].to_f
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def finish
|
|
113
|
+
@subpaths.map { |segments| segments.map { |s| offset(s) } }
|
|
114
|
+
.reject(&:empty?)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def offset(segment)
|
|
118
|
+
case segment
|
|
119
|
+
when Line
|
|
120
|
+
Line.new(shift(segment.point))
|
|
121
|
+
when Curve
|
|
122
|
+
Curve.new(shift(segment.c1), shift(segment.c2), shift(segment.point))
|
|
123
|
+
else
|
|
124
|
+
segment
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def shift(point)
|
|
129
|
+
Point.new(point.x + @dx, point.y + @dy)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|