ynl 0.2.4 → 0.4.1

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.
data/lib/ynl/models.rb CHANGED
@@ -1,12 +1,14 @@
1
1
  module Ynl
2
2
  module Models
3
- Family = ::Struct.new(:name, :protocol, :protonum, :doc,
3
+ Family = ::Struct.new(:name, :protocol, :version, :protonum, :doc,
4
4
  :consts, :enums, :flags, :structs,
5
5
  :attribute_sets, :sub_messages, :operations, :mcast_groups)
6
6
  class Family
7
7
  def resolve
8
8
  structs.transform_values! {|s| s.resolve(self) }
9
+ sub_messages.transform_values! {|s| s.resolve(self) }
9
10
  attribute_sets.transform_values! {|s| s.resolve(self) }
11
+ operations.transform_values! {|operation| operation.resolve(self) }
10
12
  self
11
13
  end
12
14
 
@@ -28,9 +30,16 @@ module Ynl
28
30
  end
29
31
 
30
32
  Const = ::Struct.new(:name, :value, :doc)
33
+ Check = ::Struct.new(:operation, :value)
31
34
 
32
35
  class SubMessage
33
- Format = ::Struct.new(:value, :attribute_set)
36
+ Format = ::Struct.new(:value, :fixed_header, :attribute_set) do
37
+ def resolve(f)
38
+ self.fixed_header = fixed_header.resolve(f) if fixed_header.is_a?(Thunk)
39
+ self.attribute_set = attribute_set.resolve(f) if attribute_set.is_a?(Thunk)
40
+ self
41
+ end
42
+ end
34
43
 
35
44
  attr_reader :name, :formats
36
45
 
@@ -40,6 +49,7 @@ module Ynl
40
49
  end
41
50
 
42
51
  def resolve(f)
52
+ formats.map! { it.resolve(f) }
43
53
  self
44
54
  end
45
55
  end
@@ -99,11 +109,16 @@ module Ynl
99
109
  end
100
110
 
101
111
  class AttributeSet
102
- Attribute = ::Struct.new(:name, :type, :value, :checks, :doc) do
112
+ Attribute = ::Struct.new(:name, :type, :value, :checks, :doc, :multi_attr, :enum, :enum_as_flags) do
103
113
  def resolve(f)
104
114
  self.type = type.resolve(f)
115
+ self.enum = enum.resolve(f) if enum
105
116
  self
106
117
  end
118
+
119
+ def multi?
120
+ multi_attr
121
+ end
107
122
  end
108
123
 
109
124
  attr_reader :name, :name_prefix, :attributes, :doc
@@ -134,36 +149,58 @@ module Ynl
134
149
  end
135
150
 
136
151
  def attributes
137
- # WORKAROUND: Some upstream specs (e.g. devlink) contain duplicate attribute
138
- # entries in subset-of attribute sets. Deduplicate by name here until the
139
- # upstream specs are fixed.
140
- @attributes.uniq {|attr| attr.fetch('name') }.map do |attr|
141
- name = attr.fetch('name')
142
- superset.attributes.find { it.name == name } or raise ParseError "No attribute with name #{name}"
143
- end
152
+ @attributes
144
153
  end
145
154
 
146
155
  def resolve(f)
156
+ attributes.map! { it.resolve(f) }
147
157
  self
158
+ rescue
159
+ raise ParseError, "Failed to resolve attribute subset: #{name}"
148
160
  end
149
161
  end
150
162
 
163
+ McastGroup = ::Struct.new(:name, :value, :doc)
164
+
151
165
  class Operation
152
- attr_reader :name, :doc, :fixed_header, :attribute_set, :doit, :dumpit
166
+ attr_reader :name, :doc, :flags, :fixed_header, :attribute_set, :doit, :dumpit, :notification
153
167
 
154
- def initialize(name:, doc:, fixed_header:, attribute_set:, doit:, dumpit:)
168
+ def initialize(name:, doc:, flags:, fixed_header:, attribute_set:, doit:, dumpit:, notification:)
155
169
  @name = name
156
170
  @doc = doc
171
+ @flags = flags
157
172
  @fixed_header = fixed_header
158
173
  @attribute_set = attribute_set
159
174
  @doit = doit
160
175
  @dumpit = dumpit
176
+ @notification = notification
161
177
  end
162
178
 
163
179
  def resolve(f)
164
180
  @doit = @doit&.resolve(f)
165
181
  @dumpit = @dumpit&.resolve(f)
182
+ @notification = @notification&.resolve(f)
183
+ self
184
+ end
185
+ end
186
+
187
+ class Notification
188
+ attr_reader :kind, :message, :source, :group
189
+
190
+ def initialize(kind:, message:, source:, group:)
191
+ @kind = kind
192
+ @message = message
193
+ @source = source
194
+ @group = group
195
+ end
196
+
197
+ def resolve(f)
198
+ @message = @message.resolve(f)
199
+ @source = f.operations.fetch(@source) if @source
200
+ @group = f.mcast_groups.fetch(@group) if @group
166
201
  self
202
+ rescue KeyError => error
203
+ raise ParseError, "Failed to resolve #{kind} notification: #{error.message}"
167
204
  end
168
205
  end
169
206
 
data/lib/ynl/parser.rb CHANGED
@@ -4,6 +4,8 @@ require_relative 'models'
4
4
 
5
5
  module Ynl
6
6
  class Parser
7
+ # @param [String, #read] source
8
+ # @rbs (String | _Readable source) -> void
7
9
  def initialize(source)
8
10
  @yaml = YAML.load(source, aliases: true)
9
11
 
@@ -13,17 +15,24 @@ module Ynl
13
15
  @structs = {}
14
16
 
15
17
  @attribute_sets = {}
18
+ @attribute_definitions = {}
16
19
  @sub_messages = {}
17
20
  @operations = {}
18
21
  @mcast_groups = {}
19
22
  end
20
23
 
24
+ # @param [String, #to_path] path
25
+ # @return [Models::Family]
26
+ # @rbs (String | _PathLike path) -> Models::Family
21
27
  def self.parse_file(path)
22
28
  File.open(path) {|f| new(f) }.parse
23
29
  end
24
30
 
31
+ # @return [Models::Family]
32
+ # @rbs () -> Models::Family
25
33
  def parse
26
34
  protocol = @yaml['protocol'] || 'genetlink'
35
+ version = @yaml.fetch('version', 1)
27
36
  protonum = @yaml['protonum']
28
37
  name = @yaml['name']
29
38
  doc = translate_doc(@yaml['doc'])
@@ -79,13 +88,14 @@ module Ynl
79
88
  end
80
89
  end
81
90
  end
82
- @yaml['mcast-groups']&.each do |d|
91
+ @yaml.dig('mcast-groups', 'list')&.each do |d|
83
92
  parse_mcast_group(d)
84
93
  end
85
94
 
86
95
  Models::Family.new(
87
96
  name:,
88
97
  protocol:,
98
+ version:,
89
99
  protonum:,
90
100
  doc:,
91
101
  consts: @consts,
@@ -106,9 +116,11 @@ module Ynl
106
116
  @consts[v.name] = v
107
117
  when 'enum'
108
118
  v = parse_enum_flags(d, type: :enum)
119
+ reject_enum_flags_name_collision(v.name, @flags)
109
120
  @enums[v.name] = v
110
121
  when 'flags'
111
122
  v = parse_enum_flags(d, type: :flags)
123
+ reject_enum_flags_name_collision(v.name, @enums)
112
124
  @flags[v.name] = v
113
125
  when 'struct'
114
126
  v = parse_struct(d)
@@ -118,19 +130,33 @@ module Ynl
118
130
  end
119
131
  end
120
132
 
133
+ private def reject_enum_flags_name_collision(name, other_definitions)
134
+ return unless other_definitions.key?(name)
135
+
136
+ raise ParseError, "Definition #{name.inspect} cannot be both an enum and flags"
137
+ end
138
+
121
139
  private def parse_const(d)
122
140
  name = "#{d['name-prefix']}#{d.fetch('name')}"
123
- Models::Const.new(name, d.fetch('value'), translate_doc(d['doc']))
141
+ value = d.fetch('value')
142
+ unless value.is_a?(Integer) || value.is_a?(String)
143
+ raise ParseError, "Constant value must be a string or an integer: #{name}"
144
+ end
145
+
146
+ Models::Const.new(name, value, translate_doc(d['doc']))
124
147
  end
125
148
 
126
149
  private def parse_enum_flags(d, type:)
127
150
  cls = type == :enum ? Models::Enum : Models::Flags
128
151
  result = cls.new(name: d.fetch('name'), doc: translate_doc(d['doc']))
129
152
 
130
- start_value = d['start-value'] || 0
131
- value = type == :enum ? start_value : 1 << start_value
153
+ start_value = d['value-start'] || 0
154
+ raw_value = start_value
132
155
 
133
156
  d.fetch('entries').each do |v|
157
+ explicit_value = v.is_a?(Hash) ? v['value'] : nil
158
+ raw_value = explicit_value unless explicit_value.nil?
159
+ value = type == :enum ? raw_value : 1 << raw_value
134
160
  case v
135
161
  when String
136
162
  entry = cls::Entry.new(name: v, value:)
@@ -142,7 +168,7 @@ module Ynl
142
168
 
143
169
  result.entries << entry
144
170
 
145
- value = type == :enum ? value + 1 : value << 1
171
+ raw_value += 1
146
172
 
147
173
  rescue
148
174
  raise ParseError, "Failed to parse enum/flags entry: #{v.fetch('name')}"
@@ -164,6 +190,7 @@ module Ynl
164
190
  when 'binary'
165
191
  Types::Binary.new(
166
192
  struct: d['struct'] ? Models::Thunk.new {|f| f.structs.fetch(d['struct']) } : nil,
193
+ length: d['len'],
167
194
  display_hint: d['display-hint'],
168
195
  )
169
196
  when 'pad'
@@ -184,10 +211,15 @@ module Ynl
184
211
  byte_order: parse_byte_order(d['byte-order']),
185
212
  )
186
213
  when 'binary'
187
- Types::Binary.new(
188
- struct: d['struct'] ? Models::Thunk.new {|f| f.structs.fetch(d.fetch('struct')) } : nil,
189
- display_hint: d['display-hint'],
190
- )
214
+ if d.key?('sub-type')
215
+ Types::PackedArray.new(sub_type: parse_packed_array_sub_type(d))
216
+ else
217
+ Types::Binary.new(
218
+ struct: d['struct'] ? Models::Thunk.new {|f| f.structs.fetch(d.fetch('struct')) } : nil,
219
+ length: nil,
220
+ display_hint: d['display-hint'],
221
+ )
222
+ end
191
223
  when 'string'
192
224
  Types::String.new
193
225
  when 'nest'
@@ -200,11 +232,12 @@ module Ynl
200
232
  )
201
233
  when 'nest-type-value'
202
234
  if d['nested-attributes']
203
- Types::NestedAttributes.new(
235
+ Types::NestTypeValue.new(
204
236
  attribute_set: Models::Thunk.new {|f| f.attribute_sets.fetch(d.fetch('nested-attributes')) },
237
+ type_values: d.fetch('type-value'),
205
238
  )
206
239
  else
207
- Types::Binary.new(struct: nil, display_hint: nil)
240
+ Types::Binary.new(struct: nil, length: nil, display_hint: nil)
208
241
  end
209
242
  when 'sub-message'
210
243
  Types::SubMessage.new(
@@ -237,6 +270,7 @@ module Ynl
237
270
  when 'binary'
238
271
  Types::Binary.new(
239
272
  struct: nil,
273
+ length: nil,
240
274
  display_hint: d['display-hint'],
241
275
  )
242
276
  when 'nest'
@@ -248,13 +282,26 @@ module Ynl
248
282
  end
249
283
  end
250
284
 
285
+ private def parse_packed_array_sub_type(d)
286
+ sub_type = d.fetch('sub-type')
287
+ case sub_type
288
+ when 'u8', 'u16', 'u32', 'u64', 's8', 's16', 's32', 's64', 'int'
289
+ Types::Scalar.new(
290
+ type: sub_type,
291
+ byte_order: parse_byte_order(d['byte-order']),
292
+ )
293
+ else
294
+ raise ParseError, "Unknown packed-array sub-type: #{sub_type}"
295
+ end
296
+ end
297
+
251
298
  private def parse_byte_order(v)
252
299
  case v
253
300
  when nil
254
301
  :host
255
302
  when 'big-endian'
256
303
  :big
257
- when 'litten-endian'
304
+ when 'little-endian'
258
305
  :little
259
306
  else
260
307
  raise ParseError, "Unknown endian: #{v}"
@@ -287,21 +334,8 @@ module Ynl
287
334
 
288
335
  private def parse_check(op, value_literal)
289
336
  case op
290
- when 'max'
291
- value = parse_value(value_literal)
292
- return %{raise ArgumentError, "Value \#{it.inspect} is greater than maximum #{value}" unless it <= #{value}}
293
- when 'min'
294
- value = parse_value(value_literal)
295
- return %{raise ArgumentError, "Value \#{it.inspect} is less than minimum #{value}" unless it >= #{value}}
296
- when 'min-len'
297
- value = parse_value(value_literal)
298
- return %{raise ArgumentError, "Value \#{it.inspect} is shorter than minimum length #{value}" unless it.bytesize >= #{value}}
299
- when 'max-len'
300
- value = parse_value(value_literal)
301
- return %{raise ArgumentError, "Value \#{it.inspect} is longer than maximum length #{value}" unless it.bytesize <= #{value}}
302
- when 'exact-len'
303
- value = parse_value(value_literal)
304
- return %{raise ArgumentError, "Value \#{it.inspect} is not equal to length #{value}" unless it.bytesize == #{value}}
337
+ when 'max', 'min', 'min-len', 'max-len', 'exact-len'
338
+ Models::Check.new(operation: op, value: parse_value(value_literal))
305
339
  when 'unterminated-ok'
306
340
  return nil
307
341
  else
@@ -321,7 +355,7 @@ module Ynl
321
355
  (2 ** 31) - 1
322
356
  when String
323
357
  const = @consts[v] or raise ParseError, "Unknown value: #{v}"
324
- const.value
358
+ const
325
359
  else
326
360
  raise ParseError, "Unknown value: #{v}"
327
361
  end
@@ -331,38 +365,72 @@ module Ynl
331
365
  name = d.fetch('name')
332
366
  if subset_of = d['subset-of']
333
367
  superset = @attribute_sets.fetch(subset_of)
334
- result = Models::AttributeSubset.new(name:, superset:, attributes: d.fetch('attributes'), doc: translate_doc(d['doc']))
368
+ inherited = @attribute_definitions.fetch(subset_of).to_h { [it.fetch('name'), it] }
369
+ definitions = d.fetch('attributes').uniq { it.fetch('name') }.map do |override|
370
+ parent = inherited.fetch(override.fetch('name'))
371
+ parent.merge(override).merge('value' => parent.fetch('value'))
372
+ end
373
+ attributes = definitions.filter_map { parse_attribute(it) }
374
+ result = Models::AttributeSubset.new(name:, superset:, attributes:, doc: translate_doc(d['doc']))
335
375
  else
336
376
  name_prefix = d['name_prefix']
337
377
  result = Models::AttributeSet.new(name:, name_prefix:, doc: translate_doc(d['doc']))
338
378
  value = 0
339
-
340
- d.fetch('attributes').each do |v|
379
+ definitions = d.fetch('attributes').map do |v|
341
380
  value = v.fetch('value', value + 1)
342
- if type = parse_attribute_type(v)
343
- attribute = Models::AttributeSet::Attribute.new(name: v.fetch('name'), type: type, value:)
344
- result.attributes << attribute
345
- if checks = v['checks']
346
- attribute.checks = parse_checks(checks)
347
- end
348
- end
349
- rescue
350
- raise ParseError, "Failed to parse attribute: #{v.fetch('name')}"
381
+ v.merge('value' => value)
351
382
  end
383
+ result.attributes.concat(definitions.filter_map { parse_attribute(it) })
352
384
  end
353
385
 
354
386
  @attribute_sets[name] = result
387
+ @attribute_definitions[name] = definitions
355
388
 
356
- rescue
357
- raise ParseError, "Failed to parse attribute set: #{d.fetch('name')}"
389
+ rescue => error
390
+ raise ParseError, "Failed to parse attribute set #{d.fetch('name')}: #{error.message}"
391
+ end
392
+
393
+ private def parse_attribute(d)
394
+ multi_attr = d.fetch('multi-attr', false)
395
+ unless multi_attr == true || multi_attr == false
396
+ raise ParseError, "multi-attr must be a boolean"
397
+ end
398
+
399
+ enum_as_flags = d.fetch('enum-as-flags', false)
400
+ unless enum_as_flags == true || enum_as_flags == false
401
+ raise ParseError, "enum-as-flags must be a boolean"
402
+ end
403
+ if enum_as_flags && !d['enum']
404
+ raise ParseError, "enum-as-flags requires enum"
405
+ end
406
+
407
+ return unless type = parse_attribute_type(d)
408
+
409
+ Models::AttributeSet::Attribute.new(
410
+ name: d.fetch('name'),
411
+ type:,
412
+ value: d.fetch('value'),
413
+ checks: (parse_checks(d['checks']) if d['checks']),
414
+ doc: translate_doc(d['doc']),
415
+ multi_attr:,
416
+ enum: d['enum']&.then { |ref| Models::Thunk.new {|f| f.find_type(ref) } },
417
+ enum_as_flags:,
418
+ )
419
+ rescue => error
420
+ raise ParseError, "Failed to parse attribute #{d.fetch('name')}: #{error.message}"
358
421
  end
359
422
 
360
423
  private def parse_sub_message(d)
361
424
  name = d.fetch('name')
362
425
  result = Models::SubMessage.new(name:)
363
426
  d.fetch('formats', []).each do |fmt|
364
- attr_set = fmt['attribute-set']&.then { @attribute_sets[it] }
365
- result.formats << Models::SubMessage::Format.new(fmt['value'], attr_set)
427
+ fixed_header = fmt['fixed-header']&.then do |ref|
428
+ Models::Thunk.new {|f| f.structs.fetch(ref) }
429
+ end
430
+ attr_set = fmt['attribute-set']&.then do |ref|
431
+ Models::Thunk.new {|f| f.attribute_sets.fetch(ref) }
432
+ end
433
+ result.formats << Models::SubMessage::Format.new(fmt.fetch('value'), fixed_header, attr_set)
366
434
  end
367
435
  @sub_messages[name] = result
368
436
  end
@@ -384,18 +452,35 @@ module Ynl
384
452
 
385
453
  doit = d['do']&.then { parse_request_reply(it, default_request_id: request_id, default_reply_id: reply_id) }
386
454
  dumpit = d['dump']&.then { parse_request_reply(it, default_request_id: request_id, default_reply_id: reply_id) }
387
-
388
- # TODO: notify
455
+ notification = parse_notification(d, default_id: reply_id)
389
456
 
390
457
  @operations[name] = Models::Operation.new(
391
- name:, doc: translate_doc(d['doc']), fixed_header:, attribute_set:,
392
- doit:, dumpit:,
458
+ name:, doc: translate_doc(d['doc']), flags: d.fetch('flags', []), fixed_header:, attribute_set:,
459
+ doit:, dumpit:, notification:,
393
460
  )
394
461
  end
395
462
 
463
+ private def parse_notification(d, default_id:)
464
+ if source = d['notify']
465
+ Models::Notification.new(
466
+ kind: :notify,
467
+ message: parse_message({}, default_id:),
468
+ source:,
469
+ group: d['mcgrp'],
470
+ )
471
+ elsif event = d['event']
472
+ Models::Notification.new(
473
+ kind: :event,
474
+ message: parse_message(event, default_id:),
475
+ source: nil,
476
+ group: d['mcgrp'],
477
+ )
478
+ end
479
+ end
480
+
396
481
  private def parse_request_reply(d, default_request_id:, default_reply_id:)
397
482
  Models::RequestReply.new(
398
- request: d['request']&.then { parse_message(it, default_id: default_request_id) },
483
+ request: parse_message(d.fetch('request', {}), default_id: default_request_id),
399
484
  reply: d['reply']&.then { parse_message(it, default_id: default_reply_id) },
400
485
  )
401
486
  end
@@ -405,6 +490,12 @@ module Ynl
405
490
  end
406
491
 
407
492
  private def parse_mcast_group(d)
493
+ name = d.fetch('name')
494
+ @mcast_groups[name] = Models::McastGroup.new(
495
+ name,
496
+ d['value'],
497
+ translate_doc(d['doc']),
498
+ )
408
499
  end
409
500
 
410
501
  private def translate_doc(doc)
data/lib/ynl/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Ynl
2
- VERSION = '0.2.4'
2
+ VERSION = '0.4.1'
3
3
  end
data/lib/ynl.rb CHANGED
@@ -1,12 +1,27 @@
1
- # Parse for YNL netlink specification
1
+ # Parser for YNL netlink specification
2
2
  #
3
3
  # See: https://www.kernel.org/doc/html/latest/userspace-api/netlink/specs.html
4
4
 
5
5
  require_relative 'ynl/family'
6
6
 
7
+ # Ynl parser and code generator. See {Family} for library entrypoint.
7
8
  module Ynl
9
+ # @rbs!
10
+ # interface _Readable
11
+ # def read: () -> String
12
+ # end
13
+ #
14
+ # interface _Writable
15
+ # def write: (*String strings) -> untyped
16
+ # end
17
+ #
18
+ # interface _PathLike
19
+ # def to_path: () -> String
20
+ # end
21
+
8
22
  class ParseError < StandardError; end
9
23
 
24
+ # @private
10
25
  module Types
11
26
  Scalar = Data.define(:type, :byte_order) do
12
27
  def resolve(f)
@@ -27,13 +42,25 @@ module Ynl
27
42
  end
28
43
  end
29
44
  Binary = Struct.new(:struct, :length, :display_hint) do
45
+ using Generator::Refinements
46
+
30
47
  def resolve(f)
31
48
  self.struct = struct.resolve(f) if self.struct
32
49
  self
33
50
  end
34
51
 
35
52
  def rbs_type
36
- 'untyped'
53
+ struct ? 'Structs::' + struct.name.as_class_name : '::String'
54
+ end
55
+ end
56
+ PackedArray = Struct.new(:sub_type) do
57
+ def resolve(f)
58
+ self.sub_type = sub_type.resolve(f)
59
+ self
60
+ end
61
+
62
+ def rbs_type
63
+ "::Array[#{sub_type.rbs_type}]"
37
64
  end
38
65
  end
39
66
  NestedAttributes = Struct.new(:attribute_set) do
@@ -48,6 +75,20 @@ module Ynl
48
75
  'AttributeSets::' + attribute_set.name.as_class_name
49
76
  end
50
77
  end
78
+ NestTypeValue = Struct.new(:attribute_set, :type_values) do
79
+ using Generator::Refinements
80
+
81
+ def resolve(f)
82
+ self.attribute_set = attribute_set.resolve(f)
83
+ self
84
+ end
85
+
86
+ def rbs_type
87
+ type = 'AttributeSets::' + attribute_set.name.as_class_name
88
+ type_values.length.times { type = "::Hash[::Integer, #{type}]" }
89
+ type
90
+ end
91
+ end
51
92
  SubMessage = Struct.new(:sub_message, :selector) do
52
93
  def resolve(f)
53
94
  self.sub_message = sub_message.resolve(f)
@@ -55,7 +96,7 @@ module Ynl
55
96
  end
56
97
 
57
98
  def rbs_type
58
- 'untyped'
99
+ '::Nl::SubMessage'
59
100
  end
60
101
  end
61
102
 
@@ -85,7 +126,7 @@ module Ynl
85
126
  end
86
127
 
87
128
  def rbs_type
88
- '::Integer'
129
+ '::Nl::Bitfield32'
89
130
  end
90
131
  end
91
132
 
@@ -96,7 +137,7 @@ module Ynl
96
137
  end
97
138
 
98
139
  def rbs_type
99
- 'untyped'
140
+ "::Array[#{sub_type.rbs_type}]"
100
141
  end
101
142
  end
102
143
  end
@@ -0,0 +1,24 @@
1
+ # Generated from lib/ynl/family.rb with RBS::Inline
2
+
3
+ module Ynl
4
+ # Entry points for generating family classes from YNL specifications.
5
+ class Family
6
+ # Builds a family class from a YNL specification file.
7
+ #
8
+ # @param [String, #to_path] path a path to a YNL specification file
9
+ # @return [Class<Nl::Family>] the generated family class
10
+ # @rbs (String | _PathLike path) -> Class
11
+ def self.build: (String | _PathLike path) -> Class
12
+
13
+ # Generates Ruby source code to define a family class from a YNL specification.
14
+ #
15
+ # @param [String, #read] source a YAML string or readable IO containing the YNL specification
16
+ # @param [#write] out the destination for the generated source code
17
+ # @option kwargs [String, nil] superclass the name of the generated class's superclass
18
+ # @option kwargs [String, nil] namespace the namespace in which to define the family class
19
+ # @option kwargs [String, nil] default_resolver the expression used as the default family resolver
20
+ # @return [String] the generated family class name
21
+ # @rbs (String | _Readable source, _Writable out, ?superclass: String?, ?namespace: String?, ?default_resolver: String?) -> String
22
+ def self.generate: (String | _Readable source, _Writable out, ?superclass: String?, ?namespace: String?, ?default_resolver: String?) -> String
23
+ end
24
+ end