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/generator.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  module Ynl
2
+ # Generates Ruby code from a parsed YNL specification.
2
3
  class Generator
4
+ # @private
3
5
  module Refinements
4
6
  WORD_DELIM = /[ _-]+/
5
7
 
@@ -32,19 +34,30 @@ module Ynl
32
34
 
33
35
  PRELUDE = -<<~RUBY
34
36
  # frozen_string_literal: true
35
- # rbs_inline: enabled
36
37
  # This code is generated by Ynl::Generator. DO NOT EDIT.
37
38
  require 'nl'
38
39
  RUBY
39
40
 
41
+ OPERATION_FLAG_DOCS = {
42
+ 'admin-perm' => 'Requires CAP_NET_ADMIN in the initial user namespace.',
43
+ 'uns-admin-perm' => 'Requires CAP_NET_ADMIN in the user namespace owning the network namespace.',
44
+ }.freeze
45
+ KERNEL_DOC_REFERENCE = %r{Documentation/(?<path>[A-Za-z0-9_./+-]+)\.rst\b}
46
+ private_constant :OPERATION_FLAG_DOCS, :KERNEL_DOC_REFERENCE
47
+
48
+ # @rbs (Models::Family ynl, _Writable out) -> void
40
49
  def initialize(ynl, out)
41
50
  @ynl = ynl
42
51
  @out = out
43
52
  @indent = 0
44
53
  end
45
54
 
46
- def generate(superclass: '::Nl::Family', namespace: nil)
47
- @protocol = '::Nl::Protocols::' + (@ynl.protocol == 'netlink-raw' ? 'Raw' : 'Genl')
55
+ # @rbs (?superclass: String?, ?namespace: String?, ?default_resolver: String?) -> void
56
+ def generate(superclass: nil, namespace: nil, default_resolver: nil)
57
+ raw = @ynl.protocol == 'netlink-raw'
58
+ @wire = raw ? '::Nl::Raw' : '::Nl::Genl'
59
+ superclass ||= raw ? '::Nl::Raw::Family' : '::Nl::Genl::Family'
60
+ build_selector_plan
48
61
 
49
62
  emit_comment(@ynl.doc)
50
63
 
@@ -52,35 +65,62 @@ module Ynl
52
65
  emit_class([*namespace, classname].join('::'), superclass) do
53
66
  emit_const('NAME', @ynl.name.as_string_literal)
54
67
 
55
- if @ynl.protocol == 'netlink-raw'
56
- emit_const('PROTOCOL', "Ractor.make_shareable(::Nl::Protocols::Raw.new(#{@ynl.name.as_string_literal}, #{@ynl.protonum}))")
57
- else
58
- emit_const('PROTOCOL', "Ractor.make_shareable(::Nl::Protocols::Genl.new(#{@ynl.name.as_string_literal}))")
68
+ emit_const('PROTONUM', @ynl.protonum) if raw
69
+ emit_const('VERSION', @ynl.version) unless raw
70
+ emit_open(raw:, default_resolver:, classname:)
71
+
72
+ emit_mcast_groups
73
+
74
+ unless @ynl.consts.empty?
75
+ emit_module('Constants') do
76
+ @ynl.consts.each_value do |const|
77
+ emit_comment(const.doc)
78
+ emit_const(const.name.as_const_name, const_value_literal(const.value))
79
+ end
80
+ end
81
+ end
82
+
83
+ emit_value_definitions('Enums', @ynl.enums.values) { |_definition, entry| entry.value }
84
+ emit_value_definitions('Flags', @ynl.flags.values + enums_used_as_flags) do |definition, entry|
85
+ definition.is_a?(Models::Enum) ? 1 << entry.value : entry.value
59
86
  end
60
87
 
88
+ deferred_struct_consts = []
61
89
  emit_module('Structs') do
62
90
  @ynl.structs.each do |name, struct|
63
91
  emit_comment(struct.doc)
64
92
  write(name.as_class_name, " = Struct.new(")
65
93
  indent do
66
94
  struct.members.each do |member|
67
- write(member.name.as_variable_name.as_symbol_literal, ', #: ', member.type.rbs_type)
95
+ write(member.name.as_variable_name.as_symbol_literal, ', #: ', struct_member_rbs_type(member.type))
68
96
  end
69
97
  end
70
98
  write(')')
71
99
  emit_class(name.as_class_name) do
72
- emit_nodoc
73
- emit_const(
74
- 'MEMBERS',
75
- "Ractor.make_shareable({#{struct.members.map { "#{it.name.as_variable_name}: #{to_datatype(it.type, nil)}" }.join(', ') }})",
76
- rbs: 'Hash[::Symbol, ::Nl::_DataType]',
77
- )
100
+ struct.members.each do |member|
101
+ emit_yard_attribute(
102
+ member.name.as_method_name,
103
+ struct_member_yard_type(member.type),
104
+ member.doc,
105
+ )
106
+ end
107
+ members = struct.members.map do |member|
108
+ "#{member.name.as_variable_name}: #{to_struct_member_datatype(member.type)}"
109
+ end
110
+ members = "Ractor.make_shareable({#{members.join(', ')}})"
111
+ if struct.members.any? { it.type.is_a?(Types::Binary) && it.type.struct }
112
+ deferred_struct_consts << ["Structs::#{name.as_class_name}::MEMBERS", members]
113
+ else
114
+ emit_const('MEMBERS', members, rbs: 'Hash[::Symbol, ::Nl::_DataType]', internal: true)
115
+ end
78
116
 
79
117
  emit_comment('Decodes the struct.')
80
118
  emit_rbs_comment(
81
119
  'decoder: ::Nl::Decoder',
82
120
  'return: instance',
83
121
  )
122
+ emit_yard_param('decoder', 'Nl::Decoder')
123
+ emit_yard_return('self')
84
124
  write('def self.decode(decoder)')
85
125
  indent do
86
126
  write('self.new(*MEMBERS.map {|name, datatype| datatype.decode(decoder) })')
@@ -92,6 +132,8 @@ module Ynl
92
132
  'encoder: ::Nl::Encoder',
93
133
  'return: void',
94
134
  )
135
+ emit_yard_param('encoder', 'Nl::Encoder')
136
+ emit_yard_return('void')
95
137
  write('def encode(encoder)')
96
138
  indent do
97
139
  write('MEMBERS.each {|name, datatype| datatype.encode(encoder, self.public_send(name)) }')
@@ -100,49 +142,91 @@ module Ynl
100
142
  end
101
143
  end
102
144
  end
145
+ deferred_struct_consts.each do |name, members|
146
+ emit_const(name, members, rbs: 'Hash[::Symbol, ::Nl::_DataType]', internal: true)
147
+ end
103
148
 
149
+ deferred_sub_message_datatypes = []
104
150
  emit_module('AttributeSets') do
105
151
  deferred_consts = []
106
152
  @ynl.attribute_sets.each do |name, attr_set|
107
153
  emit_comment(attr_set.doc)
108
- emit_class(name.as_class_name, @protocol + '::AttributeSet') do
154
+ emit_class(name.as_class_name, '::Nl::AttributeSet') do
109
155
  emit_comment("Abstract class")
110
- emit_class('Attribute', @protocol + '::AttributeSet::Attribute') do
156
+ emit_class('Attribute', '::Nl::AttributeSet::Attribute') do
111
157
  end
112
158
  attr_set.attributes.each do |attr|
113
- emit_comment(attr.doc)
159
+ emit_comment(attribute_doc(attr))
114
160
  emit_class(attr.name.as_class_name, 'Attribute') do
115
161
  emit_const('TYPE', attr.value)
116
162
  emit_const('NAME', attr.name.as_variable_name.as_symbol_literal)
117
- if attr.type.is_a?(Types::NestedAttributes) ||
163
+ emit_const('MULTI', 'true', internal: true) if attr.multi?
164
+ emit_const('ORDER', @attribute_orders.fetch(attr_set).fetch(attr), internal: true)
165
+ if slot = @local_selectors.fetch(attr_set).index(attr.name)
166
+ emit_const('SELECTOR_SLOT', slot, internal: true)
167
+ end
168
+ if attr.type.is_a?(Types::SubMessage)
169
+ deferred_sub_message_datatypes << [
170
+ "AttributeSets::#{name.as_class_name}::#{attr.name.as_class_name}::DATATYPE",
171
+ to_datatype(attr.type, attr.checks, owner: attr_set),
172
+ ]
173
+ elsif attr.type.is_a?(Types::NestedAttributes) ||
174
+ attr.type.is_a?(Types::NestTypeValue) ||
118
175
  (attr.type.is_a?(Types::IndexedArray) && attr.type.sub_type.is_a?(Types::NestedAttributes))
119
- deferred_consts << ["#{name.as_class_name}::#{attr.name.as_class_name}::DATATYPE", to_datatype(attr.type, attr.checks)]
176
+ deferred_consts << ["#{name.as_class_name}::#{attr.name.as_class_name}::DATATYPE", to_datatype(attr.type, attr.checks, owner: attr_set)]
120
177
  else
121
- emit_const('DATATYPE', to_datatype(attr.type, attr.checks))
178
+ emit_const('DATATYPE', to_datatype(attr.type, attr.checks, owner: attr_set), internal: true)
122
179
  end
123
180
  end
124
181
  end
125
182
 
126
- emit_nodoc
183
+ selector_names = @local_selectors.fetch(attr_set)
184
+ external_selector_names = @external_selectors.fetch(attr_set)
185
+ unless selector_names.empty? && external_selector_names.empty?
186
+ emit_const(
187
+ 'SELECTOR_NAMES',
188
+ "Ractor.make_shareable({local: [#{selector_names.map { it.as_variable_name.as_symbol_literal }.join(', ')}], external: [#{external_selector_names.map { it.as_variable_name.as_symbol_literal }.join(', ')}]})",
189
+ internal: true,
190
+ )
191
+ end
192
+
127
193
  emit_const(
128
194
  'BY_NAME',
129
195
  "Ractor.make_shareable({#{attr_set.attributes.map { "#{it.name.as_variable_name.as_symbol_literal} => #{it.name.as_class_name}" }.join(', ') }})",
130
196
  rbs: 'Hash[::Symbol, Attribute]',
197
+ internal: true,
131
198
  )
132
199
 
133
- emit_nodoc
134
200
  emit_const(
135
201
  'BY_TYPE',
136
202
  "Ractor.make_shareable({#{attr_set.attributes.map { "#{it.value} => #{it.name.as_class_name}" }.join(', ') }})",
137
203
  rbs: 'Hash[::Integer, Attribute]',
204
+ internal: true,
138
205
  )
139
206
 
207
+ attr_set.attributes.each do |attribute|
208
+ next if attribute.type.is_a?(Types::Pad)
209
+
210
+ emit_comment(attribute_doc(attribute))
211
+ emit_rbs_comment('return: ' + attribute_rbs_type(attribute))
212
+ emit_yard_return(attribute_yard_type(attribute))
213
+ emit_yard_see(attribute.name.as_class_name)
214
+ expression = if attribute.multi?
215
+ "self[#{attribute.name.as_variable_name.as_symbol_literal}].map(&:value)"
216
+ else
217
+ "self[#{attribute.name.as_variable_name.as_symbol_literal}]&.value"
218
+ end
219
+ emit_getter(attribute.name.as_method_name, expression)
220
+ end
221
+
140
222
  emit_singleton_class do
141
223
  emit_comment('Looks up Attribute class by name.')
142
224
  emit_rbs_comment(
143
225
  'name: Symbol',
144
226
  'return: Attribute',
145
227
  )
228
+ emit_yard_param('name', 'Symbol')
229
+ emit_yard_return('Attribute')
146
230
  emit_getter('by_name(name)', 'BY_NAME.fetch(name)')
147
231
 
148
232
  emit_comment('Looks up Attribute class by type value.')
@@ -150,11 +234,21 @@ module Ynl
150
234
  'type: Integer',
151
235
  'return: Attribute',
152
236
  )
237
+ emit_yard_param('type', 'Integer')
238
+ emit_yard_return('Attribute')
153
239
  emit_getter('by_type(type)', 'BY_TYPE.fetch(type)')
154
240
  end
155
241
  end
156
242
  end
157
- deferred_consts.each { emit_const(*it) }
243
+ deferred_consts.each do |const|
244
+ emit_const(*const, internal: true)
245
+ end
246
+ end
247
+
248
+
249
+ emit_sub_messages
250
+ deferred_sub_message_datatypes.each do |const|
251
+ emit_const(*const, internal: true)
158
252
  end
159
253
 
160
254
  emit_module('Messages') do
@@ -162,58 +256,44 @@ module Ynl
162
256
  %w[do dump].each do |method|
163
257
  if request_reply = oper.public_send(method + 'it')
164
258
  %w[request reply].to_h { [it, request_reply.public_send(it)] }.compact.each do |type, msg|
165
- emit_comment(oper.doc)
166
- emit_class(method.as_class_name + oper.name.as_class_name + type.as_class_name, "#{@protocol}::Message") do
167
- emit_const('TYPE', msg.value)
168
- emit_const('FIXED_HEADER', oper.fixed_header&.then { 'Structs::' + it.name.as_class_name } || 'nil')
169
- emit_const('ATTRIBUTE_SET', "AttributeSets::#{oper.attribute_set.name.as_class_name}")
170
- params = msg.attributes
171
- attribute_params = oper.attribute_set.attributes.map(&:name) & params
172
- emit_const('ATTRIBUTES', "Ractor.make_shareable(%i[#{attribute_params.map { it.as_variable_name }.join(' ')}])")
173
- oper.fixed_header.members.each do |member|
174
- param = member.name
175
- datatype = member.type
176
- next if datatype.is_a? Types::Pad
177
- next if attribute_params.include?(param)
178
- emit_comment("Gets the value of `#{param}` field in the message's fixed header.")
179
- emit_rbs_comment(
180
- 'return: ' + datatype.rbs_type,
181
- )
182
- emit_getter(param.as_method_name, "fixed_header.#{param.as_method_name}")
183
- end if oper.fixed_header
184
- attribute_params.each do |param|
185
- datatype = oper.attribute_set.attributes.find { it.name == param }.type
186
- next if datatype.is_a? Types::Pad
187
- extending = oper.fixed_header&.members&.any? { it.name == param }
188
-
189
- if extending
190
- emit_comment("Gets the value of `#{param}` attribute or fixed header in the message.")
191
- else
192
- emit_comment("Gets the value of `#{param}` attribute in the message.")
193
- end
194
- emit_rbs_comment(
195
- 'return: ' + datatype.rbs_type,
196
- )
197
- if extending
198
- # If the fixed header and the attribute set have the same-name parameter, the value from the attribute should have
199
- # the precedence over that from the header.
200
- emit_getter(param.as_method_name, "attributes[#{param.as_variable_name.as_symbol_literal}]&.value || fixed_header.#{param.as_method_name}")
201
- else
202
- emit_getter(param.as_method_name, "attributes[#{param.as_variable_name.as_symbol_literal}]&.value")
203
- end
204
- end
205
- end
259
+ emit_message_class(
260
+ method.as_class_name + oper.name.as_class_name + type.as_class_name,
261
+ msg,
262
+ fixed_header: oper.fixed_header,
263
+ attribute_set: oper.attribute_set,
264
+ doc: operation_doc(oper),
265
+ )
206
266
  end
207
267
  end
208
268
  end
209
269
  end
210
270
  end
211
271
 
272
+ emit_module('Notifications') do
273
+ notification_operations.each do |oper|
274
+ message, fixed_header, attribute_set = notification_layout(oper)
275
+ emit_message_class(
276
+ oper.name.as_class_name,
277
+ message,
278
+ fixed_header:,
279
+ attribute_set:,
280
+ doc: notification_doc(oper),
281
+ )
282
+ end
283
+ end
284
+
285
+ emit_const(
286
+ 'NOTIFICATIONS',
287
+ "Ractor.make_shareable({#{notification_operations.map { |oper| "#{oper.notification.message.value} => Notifications::#{oper.name.as_class_name}" }.join(', ') }})",
288
+ rbs: 'Hash[::Integer, ::Class]',
289
+ internal: true,
290
+ )
291
+
212
292
  # emit request methods
213
293
  @ynl.operations.each do |name, oper|
214
294
  %w[do dump].each do |method|
215
295
  if request_reply = oper.public_send(method + 'it')
216
- next unless request = request_reply.request # FIXME: what should we do in this case?
296
+ request = request_reply.request
217
297
 
218
298
  request_class = "Messages::#{method.as_class_name}#{oper.name.as_class_name}Request"
219
299
  reply_class = request_reply.reply ? "Messages::#{method.as_class_name}#{oper.name.as_class_name}Reply" : 'nil'
@@ -224,15 +304,28 @@ module Ynl
224
304
  params = header_params + attribute_params
225
305
  params.reject! { it.type.is_a? Types::Pad }
226
306
 
227
- rbs_params = params.map { |it| "?#{it.name.as_variable_name}: #{it.type.rbs_type}" }.join(', ')
307
+ rbs_params = params.map { |it| "?#{it.name.as_variable_name}: #{parameter_rbs_type(it)}" }.join(', ')
228
308
  rbs_result = request_reply.reply ? reply_class : 'void'
229
309
 
230
- emit_comment(oper.doc)
310
+ emit_comment(operation_doc(oper))
231
311
  if method == 'dump'
312
+ method_name = "#{method.as_method_name}_#{oper.name.as_method_name}"
232
313
  emit_rbs_comment(
233
- "(#{rbs_params}) -> Enumerable[#{rbs_result}]\n | (#{rbs_params}) { (#{rbs_result}) -> void } -> void",
314
+ "(#{rbs_params}) -> Array[#{rbs_result}]\n | (#{rbs_params}) { (#{rbs_result}) -> void } -> void",
234
315
  )
235
- write("def #{method.as_method_name}_#{oper.name.as_method_name}(**args, &block)")
316
+ emit_yard_overload("#{method_name}(**args)") do |indentation|
317
+ emit_comment("#{indentation}Returns an array when no block is given.")
318
+ emit_yard_options(params, indentation:)
319
+ emit_yard_return("Array<#{rbs_result}>", indentation:)
320
+ end
321
+ emit_yard_overload("#{method_name}(**args, &block)") do |indentation|
322
+ emit_comment("#{indentation}Yields each reply when a block is given.")
323
+ emit_yard_options(params, indentation:)
324
+ emit_yard_yieldparam('reply', rbs_result, indentation:)
325
+ emit_yard_return('void', indentation:)
326
+ end
327
+ emit_yard_see(request_class)
328
+ write("def #{method_name}(**args, &block)")
236
329
  indent do
237
330
  write("exchange_message(#{method.as_symbol_literal}, #{request_class}, #{reply_class}, args, &block)")
238
331
  end
@@ -240,6 +333,9 @@ module Ynl
240
333
  emit_rbs_comment(
241
334
  "(#{rbs_params}) -> #{rbs_result}",
242
335
  )
336
+ emit_yard_options(params)
337
+ emit_yard_return(rbs_result)
338
+ emit_yard_see(request_class)
243
339
  write("def #{method.as_method_name}_#{oper.name.as_method_name}(**args)")
244
340
  indent do
245
341
  write("exchange_message(#{method.as_symbol_literal}, #{request_class}, #{reply_class}, args)")
@@ -249,6 +345,56 @@ module Ynl
249
345
  end
250
346
  end
251
347
  end
348
+
349
+ emit_class('AsyncOperations') do
350
+ emit_internal
351
+ write('def initialize(&exchange)')
352
+ indent do
353
+ write('@exchange = exchange')
354
+ end
355
+ write('end')
356
+
357
+ @ynl.operations.each do |name, oper|
358
+ %w[do dump].each do |method|
359
+ next unless request_reply = oper.public_send(method + 'it')
360
+
361
+ request_class = "Messages::#{method.as_class_name}#{oper.name.as_class_name}Request"
362
+ reply_class = request_reply.reply ? "Messages::#{method.as_class_name}#{oper.name.as_class_name}Reply" : 'nil'
363
+ header_params = oper.fixed_header&.members || []
364
+ attribute_params = oper.attribute_set.attributes.filter { request_reply.request.attributes.include?(it.name) }
365
+ attribute_params.reject! {|a| header_params.any? {|h| h.name == a.name } }
366
+ params = (header_params + attribute_params).reject { it.type.is_a? Types::Pad }
367
+ rbs_params = params.map { |it| "?#{it.name.as_variable_name}: #{parameter_rbs_type(it)}" }.join(', ')
368
+ result = request_reply.reply ? reply_class : 'void'
369
+ operation = method == 'dump' ? "::Nl::Async::Stream[#{result}]" : "::Nl::Async::Future[#{result}]"
370
+ yard_operation = method == 'dump' ? "Nl::Async::Stream<#{result}>" : "Nl::Async::Future<#{result}>"
371
+
372
+ emit_comment(operation_doc(oper))
373
+ emit_rbs_comment("(#{rbs_params}) -> #{operation}")
374
+ emit_yard_options(params)
375
+ emit_yard_return(yard_operation)
376
+ emit_yard_see(request_class)
377
+ write("def #{method.as_method_name}_#{oper.name.as_method_name}(**args)")
378
+ indent do
379
+ write("@exchange.call(#{method.as_symbol_literal}, #{request_class}, #{reply_class}, args)")
380
+ end
381
+ write('end')
382
+ end
383
+ end
384
+ end
385
+
386
+ emit_comment('Returns the asynchronous operation facade for this family.')
387
+ emit_rbs_comment('(?stream_capacity: ::Integer?) -> AsyncOperations')
388
+ emit_yard_param('stream_capacity', '::Integer, nil')
389
+ emit_yard_return('AsyncOperations')
390
+ write('def async(stream_capacity: nil)')
391
+ indent do
392
+ write('return @async_operations ||= build_async_facade(AsyncOperations) if stream_capacity.nil?')
393
+ write('build_async_facade(AsyncOperations, stream_capacity:)')
394
+ end
395
+ write('end')
396
+
397
+ emit_subscription_methods unless @ynl.mcast_groups.empty?
252
398
  end
253
399
 
254
400
  classname
@@ -291,64 +437,808 @@ module Ynl
291
437
  write('end')
292
438
  end
293
439
 
294
- private def emit_const(name, value, rbs: nil)
440
+ private def emit_const(name, value, rbs: nil, internal: false)
441
+ emit_internal if internal
295
442
  write(name, ' = ', value, *([' #: ', rbs] if rbs))
296
443
  end
297
444
 
445
+ private def emit_value_definitions(namespace, definitions)
446
+ return if definitions.empty?
447
+
448
+ emit_module(namespace) do
449
+ definitions.each do |definition|
450
+ emit_comment(definition.doc)
451
+ emit_module(definition.name.as_class_name) do
452
+ definition.entries.each do |entry|
453
+ emit_comment(entry.doc)
454
+ emit_const(entry.name.as_class_name, yield(definition, entry))
455
+ end
456
+ end
457
+ end
458
+ end
459
+ end
460
+
461
+ private def enums_used_as_flags
462
+ @ynl.attribute_sets.each_value.flat_map(&:attributes).filter_map do |attribute|
463
+ next unless attribute.enum_as_flags && attribute.enum.is_a?(Models::Enum)
464
+
465
+ attribute.enum
466
+ end.uniq
467
+ end
468
+
298
469
  private def emit_getter(name, expr)
299
470
  write('def ', name, '; ', expr, '; end')
300
471
  end
301
472
 
302
- private def emit_nodoc
303
- write('# :nodoc:')
473
+ private def emit_internal
474
+ write('# @private')
475
+ end
476
+
477
+ private def emit_open(raw:, default_resolver:, classname:)
478
+ resolver_rbs = unless raw
479
+ type = '^(::Nl::Genl::Client, ::String) -> ::Nl::Genl::FamilyInfo'
480
+ default_resolver ? "?resolver: #{type}" : "resolver: #{type}"
481
+ end
482
+ rbs_params = [
483
+ resolver_rbs,
484
+ '?executor: executor?',
485
+ '?notification_capacity: ::Integer?',
486
+ ].compact.join(', ')
487
+ emit_rbs_comment(
488
+ "(#{rbs_params}) -> (::Nl::Family::Session & instance)",
489
+ "| [R] (#{rbs_params}) { (instance) -> R } -> R",
490
+ )
491
+
492
+ resolver_ruby = unless raw
493
+ default_resolver ? "resolver: #{default_resolver}" : 'resolver:'
494
+ end
495
+ ruby_params = [
496
+ resolver_ruby,
497
+ 'executor: nil',
498
+ 'notification_capacity: DEFAULT_NOTIFICATION_CAPACITY',
499
+ ].compact.join(', ')
500
+ emit_yard_overload("open(#{ruby_params})") do |indentation|
501
+ emit_yard_open_params(resolver: !raw, indentation:)
502
+ emit_yard_return(classname, indentation:)
503
+ end
504
+ emit_yard_overload("open(#{ruby_params}, &block)") do |indentation|
505
+ emit_yard_open_params(resolver: !raw, indentation:)
506
+ emit_yard_yieldparam('family', classname, indentation:)
507
+ emit_yard_return('Object', 'the result of the block', indentation:)
508
+ end
509
+ write("def self.open(#{ruby_params})")
510
+ indent { write('super') }
511
+ write('end')
512
+ end
513
+
514
+ private def emit_yard_open_params(resolver:, indentation: '')
515
+ emit_yard_param('resolver', '#call', indentation:) if resolver
516
+ emit_yard_param('executor', 'Symbol, nil', indentation:)
517
+ emit_yard_param('notification_capacity', 'Integer, nil', indentation:)
304
518
  end
305
519
 
306
520
  private def emit_comment(comment)
307
521
  return unless comment
522
+ comment = comment.gsub(KERNEL_DOC_REFERENCE) do |reference|
523
+ "{https://docs.kernel.org/#{$~[:path]}.html #{reference}}"
524
+ end
308
525
  comment.each_line(chomp: true) do |line|
309
526
  write('# ', line)
310
527
  end
311
528
  end
312
529
 
530
+ private def operation_doc(operation)
531
+ flag_docs = operation.flags.filter_map { OPERATION_FLAG_DOCS[it] }
532
+ [operation.doc, *flag_docs].compact.join("\n\n")
533
+ end
534
+
535
+ private def notification_operations
536
+ @notification_operations ||= @ynl.operations.values.select(&:notification)
537
+ end
538
+
539
+ private def notification_doc(operation)
540
+ group = operation.notification.group
541
+ [operation.doc, ("Multicast group: `#{group.name}`." if group)].compact.join("\n\n")
542
+ end
543
+
544
+ private def notification_layout(operation)
545
+ notification = operation.notification
546
+ if notification.kind == :event
547
+ [notification.message, operation.fixed_header, operation.attribute_set]
548
+ else
549
+ source = notification.source
550
+ mode = source.doit || source.dumpit
551
+ reply = mode&.reply
552
+ unless reply
553
+ selected = source.doit ? 'do' : 'dump'
554
+ raise ParseError,
555
+ "Notification #{operation.name.inspect} references #{source.name.inspect}, which has no #{selected} reply"
556
+ end
557
+ message = Models::Message.new(
558
+ value: notification.message.value,
559
+ attributes: reply.attributes,
560
+ )
561
+ [message, source.fixed_header, source.attribute_set]
562
+ end
563
+ end
564
+
565
+ private def emit_message_class(name, message, fixed_header:, attribute_set:, doc:)
566
+ emit_comment(doc)
567
+ emit_class(name, "#@wire::Message") do
568
+ emit_const('TYPE', message.value)
569
+ emit_const('FIXED_HEADER', fixed_header&.then { 'Structs::' + it.name.as_class_name } || 'nil')
570
+ emit_const('ATTRIBUTE_SET', "AttributeSets::#{attribute_set.name.as_class_name}")
571
+ attribute_params = attribute_set.attributes.map(&:name) & message.attributes
572
+ emit_const('ATTRIBUTES', "Ractor.make_shareable(%i[#{attribute_params.map { it.as_variable_name }.join(' ')}])")
573
+ fixed_header&.members&.each do |member|
574
+ param = member.name
575
+ datatype = member.type
576
+ next if datatype.is_a? Types::Pad
577
+ next if attribute_params.include?(param)
578
+ emit_comment(member.doc)
579
+ emit_rbs_comment('return: ' + struct_member_rbs_type(datatype))
580
+ emit_yard_return(struct_member_yard_type(datatype))
581
+ emit_getter(param.as_method_name, "fixed_header.#{param.as_method_name}")
582
+ end
583
+ attribute_params.each do |param|
584
+ attribute = attribute_set.attributes.find { it.name == param }
585
+ datatype = attribute.type
586
+ next if datatype.is_a? Types::Pad
587
+ extending = fixed_header&.members&.any? { it.name == param }
588
+
589
+ if extending && attribute.multi?
590
+ raise ParseError, "Multi-attribute #{param.inspect} conflicts with a fixed-header member"
591
+ end
592
+
593
+ emit_comment(attribute_doc(attribute))
594
+ emit_rbs_comment('return: ' + attribute_rbs_type(attribute))
595
+ emit_yard_return(attribute_yard_type(attribute))
596
+ emit_yard_see("AttributeSets::#{attribute_set.name.as_class_name}::#{attribute.name.as_class_name}")
597
+ if attribute.multi?
598
+ emit_getter(param.as_method_name, "attributes[#{param.as_variable_name.as_symbol_literal}].map(&:value)")
599
+ next
600
+ end
601
+ if extending
602
+ emit_getter(param.as_method_name, "attributes[#{param.as_variable_name.as_symbol_literal}]&.value || fixed_header.#{param.as_method_name}")
603
+ else
604
+ emit_getter(param.as_method_name, "attributes[#{param.as_variable_name.as_symbol_literal}]&.value")
605
+ end
606
+ end
607
+ end
608
+ end
609
+
610
+ private def emit_sub_messages
611
+ return if @ynl.sub_messages.empty?
612
+
613
+ emit_module('SubMessages') do
614
+ @ynl.sub_messages.each_value do |sub_message|
615
+ grouped = sub_message.formats.group_by { it.value.as_class_name }
616
+ if collision = grouped.find { |_name, formats| formats.length > 1 }
617
+ ruby_name, formats = collision
618
+ raise ParseError,
619
+ "Sub-message formats #{formats.map(&:value).map(&:inspect).join(' and ')} normalize to #{ruby_name.inspect}"
620
+ end
621
+ emit_module(sub_message.name.as_class_name) do
622
+ sub_message.formats.each do |format|
623
+ emit_sub_message_class(format.value.as_class_name, format)
624
+ end
625
+ end
626
+ end
627
+ end
628
+ end
629
+
630
+ private def emit_sub_message_class(name, format)
631
+ emit_class(name, '::Nl::SubMessage') do
632
+ emit_const('FIXED_HEADER', format.fixed_header&.then { 'Structs::' + it.name.as_class_name } || 'nil')
633
+ emit_const('ATTRIBUTE_SET', format.attribute_set&.then { 'AttributeSets::' + it.name.as_class_name } || 'nil')
634
+
635
+ format.fixed_header&.members&.each do |member|
636
+ next if member.type.is_a?(Types::Pad)
637
+ extending = format.attribute_set&.attributes&.any? { it.name == member.name }
638
+ next if extending
639
+ emit_comment(member.doc)
640
+ emit_rbs_comment('return: ' + struct_member_rbs_type(member.type))
641
+ emit_yard_return(struct_member_yard_type(member.type))
642
+ emit_getter(member.name.as_method_name, "fixed_header.#{member.name.as_method_name}")
643
+ end
644
+
645
+ format.attribute_set&.attributes&.each do |attribute|
646
+ next if attribute.type.is_a?(Types::Pad)
647
+ extending = format.fixed_header&.members&.any? { it.name == attribute.name }
648
+ emit_comment(attribute_doc(attribute))
649
+ emit_rbs_comment('return: ' + attribute_rbs_type(attribute))
650
+ emit_yard_return(attribute_yard_type(attribute))
651
+ emit_yard_see("AttributeSets::#{format.attribute_set.name.as_class_name}::#{attribute.name.as_class_name}")
652
+ expression = if attribute.multi?
653
+ "attributes[#{attribute.name.as_variable_name.as_symbol_literal}].map(&:value)"
654
+ elsif extending
655
+ "attributes[#{attribute.name.as_variable_name.as_symbol_literal}]&.value || fixed_header.#{attribute.name.as_method_name}"
656
+ else
657
+ "attributes[#{attribute.name.as_variable_name.as_symbol_literal}]&.value"
658
+ end
659
+ emit_getter(attribute.name.as_method_name, expression)
660
+ end
661
+ end
662
+ end
663
+
664
+ private def parameter_rbs_type(param)
665
+ if param.is_a?(Models::AttributeSet::Attribute)
666
+ type = input_rbs_type(param.type)
667
+ param.multi? ? "::Array[#{type}]" : type
668
+ else
669
+ struct_member_rbs_type(param.type)
670
+ end
671
+ end
672
+
673
+ private def parameter_yard_type(param)
674
+ if param.is_a?(Models::AttributeSet::Attribute)
675
+ type = input_yard_type(param.type)
676
+ param.multi? ? "Array<#{type}>" : type
677
+ else
678
+ struct_member_yard_type(param.type)
679
+ end
680
+ end
681
+
682
+ private def struct_member_rbs_type(type)
683
+ if type.is_a?(Types::Binary) && type.struct
684
+ 'Structs::' + type.struct.name.as_class_name
685
+ else
686
+ type.rbs_type
687
+ end
688
+ end
689
+
690
+ private def struct_member_yard_type(type)
691
+ if type.is_a?(Types::Binary) && type.struct
692
+ 'Structs::' + type.struct.name.as_class_name
693
+ else
694
+ yard_type(type)
695
+ end
696
+ end
697
+
698
+ private def input_rbs_type(type)
699
+ case type
700
+ when Types::NestedAttributes
701
+ "(#{type.rbs_type} | ::Hash[::Symbol, untyped])"
702
+ when Types::Binary
703
+ type.struct ? "(#{type.rbs_type} | ::Hash[::Symbol, untyped])" : type.rbs_type
704
+ when Types::PackedArray
705
+ "::Array[#{input_rbs_type(type.sub_type)}]"
706
+ when Types::NestTypeValue
707
+ value_type = "(AttributeSets::#{type.attribute_set.name.as_class_name} | ::Hash[::Symbol, untyped])"
708
+ type.type_values.length.times { value_type = "::Hash[::Integer, #{value_type}]" }
709
+ value_type
710
+ when Types::IndexedArray
711
+ "::Array[#{input_rbs_type(type.sub_type)}]"
712
+ when Types::SubMessage
713
+ "(#{sub_message_rbs_type(type)} | ::Hash[::Symbol, untyped])"
714
+ else
715
+ type.rbs_type
716
+ end
717
+ end
718
+
719
+ private def input_yard_type(type)
720
+ case type
721
+ when Types::NestedAttributes
722
+ "#{yard_type(type)}, Hash<Symbol, Object>"
723
+ when Types::Binary
724
+ type.struct ? "#{yard_type(type)}, Hash<Symbol, Object>" : yard_type(type)
725
+ when Types::PackedArray, Types::IndexedArray
726
+ "Array<#{input_yard_type(type.sub_type)}>"
727
+ when Types::NestTypeValue
728
+ value_type = "AttributeSets::#{type.attribute_set.name.as_class_name}, Hash<Symbol, Object>"
729
+ type.type_values.length.times { value_type = "Hash<Integer, #{value_type}>" }
730
+ value_type
731
+ when Types::SubMessage
732
+ "#{sub_message_yard_type(type)}, Hash<Symbol, Object>"
733
+ else
734
+ yard_type(type)
735
+ end
736
+ end
737
+
738
+ private def attribute_rbs_type(attribute)
739
+ type = if attribute.type.is_a?(Types::SubMessage)
740
+ sub_message_rbs_type(attribute.type)
741
+ else
742
+ attribute.type.rbs_type
743
+ end
744
+ attribute.multi? ? "::Array[#{type}]" : type
745
+ end
746
+
747
+ private def attribute_yard_type(attribute)
748
+ type = if attribute.type.is_a?(Types::SubMessage)
749
+ sub_message_yard_type(attribute.type)
750
+ else
751
+ yard_type(attribute.type)
752
+ end
753
+ attribute.multi? ? "Array<#{type}>" : type
754
+ end
755
+
756
+ private def attribute_doc(attribute)
757
+ return attribute.doc unless attribute.enum
758
+
759
+ definition = "{#{attribute_value_definition(attribute)}}"
760
+ value_doc = if attribute.enum_as_flags || attribute.enum.is_a?(Models::Flags)
761
+ "Known flags are defined in #{definition}."
762
+ else
763
+ "Known enum values are defined in #{definition}."
764
+ end
765
+ [attribute.doc, value_doc].compact.reject(&:empty?).join("\n\n")
766
+ end
767
+
768
+ private def attribute_value_definition(attribute)
769
+ namespace = if attribute.enum_as_flags || attribute.enum.is_a?(Models::Flags)
770
+ 'Flags'
771
+ else
772
+ 'Enums'
773
+ end
774
+ "#{namespace}::#{attribute.enum.name.as_class_name}"
775
+ end
776
+
777
+ private def sub_message_rbs_type(type)
778
+ formats = type.sub_message.formats.map do |format|
779
+ "SubMessages::#{type.sub_message.name.as_class_name}::#{format.value.as_class_name}"
780
+ end
781
+ (formats << '::Nl::RawSubMessage').join(' | ')
782
+ end
783
+
784
+ private def sub_message_yard_type(type)
785
+ formats = type.sub_message.formats.map do |format|
786
+ "SubMessages::#{type.sub_message.name.as_class_name}::#{format.value.as_class_name}"
787
+ end
788
+ (formats << 'Nl::RawSubMessage').join(', ')
789
+ end
790
+
791
+ private def yard_type(type)
792
+ case type
793
+ when Types::Scalar, Types::Flag
794
+ 'Integer'
795
+ when Types::String
796
+ 'String'
797
+ when Types::Binary
798
+ type.struct ? 'Structs::' + type.struct.name.as_class_name : 'String'
799
+ when Types::PackedArray, Types::IndexedArray
800
+ "Array<#{yard_type(type.sub_type)}>"
801
+ when Types::NestedAttributes
802
+ 'AttributeSets::' + type.attribute_set.name.as_class_name
803
+ when Types::NestTypeValue
804
+ value_type = 'AttributeSets::' + type.attribute_set.name.as_class_name
805
+ type.type_values.length.times { value_type = "Hash<Integer, #{value_type}>" }
806
+ value_type
807
+ when Types::SubMessage
808
+ 'Nl::SubMessage'
809
+ when Types::Pad
810
+ 'nil'
811
+ when Types::Bitfield32
812
+ 'Nl::Bitfield32'
813
+ else
814
+ raise "Unknown YARD type: #{type.class}"
815
+ end
816
+ end
817
+
818
+ private def emit_mcast_groups
819
+ groups = @ynl.mcast_groups.values
820
+ grouped = groups.group_by { it.name.as_variable_name }
821
+ if collision = grouped.find { |_name, matches| matches.length > 1 }
822
+ ruby_name, matches = collision
823
+ raise ParseError,
824
+ "Multicast groups #{matches.map(&:name).map(&:inspect).join(' and ')} normalize to #{ruby_name.to_sym.inspect}"
825
+ end
826
+
827
+ entries = groups.map do |group|
828
+ key = group.name.as_variable_name.as_symbol_literal
829
+ value = "::Nl::McastGroup.new(#{group.name.as_string_literal}, #{mcast_group_value_literal(group.value)})"
830
+ "#{key} => #{value}"
831
+ end
832
+ emit_const(
833
+ 'MCAST_GROUPS',
834
+ "Ractor.make_shareable({#{entries.join(', ')}})",
835
+ rbs: 'Hash[::Symbol, ::Nl::McastGroup]',
836
+ internal: true,
837
+ )
838
+ end
839
+
840
+ private def emit_subscription_methods
841
+ groups = @ynl.mcast_groups.values
842
+ .map { ":#{it.name.as_variable_name}" }
843
+ emit_rbs_comment("(*(#{groups.join(' | ')}) groups) -> self")
844
+ emit_yard_param('groups', "Array<#{groups.join(', ')}>")
845
+ emit_yard_return('self')
846
+ write('def subscribe(*groups) = super')
847
+ emit_rbs_comment("(*(#{groups.join(' | ')}) groups) -> self")
848
+ emit_yard_param('groups', "Array<#{groups.join(', ')}>")
849
+ emit_yard_return('self')
850
+ write('def unsubscribe(*groups) = super')
851
+ end
852
+
313
853
  private def emit_rbs_comment(*args)
314
- write('#--')
315
854
  args.each do |arg|
316
855
  emit_comment('@rbs ' + arg)
317
856
  end
318
857
  end
319
858
 
320
- private def to_datatype(type, checks)
859
+ private def emit_yard_overload(signature)
860
+ emit_comment("@overload #{signature}")
861
+ yield ' '
862
+ end
863
+
864
+ private def emit_yard_options(params, indentation: '')
865
+ emit_yard_param('args', 'Hash', indentation:)
866
+ params.each do |param|
867
+ emit_yard_option(
868
+ 'args',
869
+ param.name.as_variable_name,
870
+ parameter_yard_type(param),
871
+ param.is_a?(Models::AttributeSet::Attribute) ? attribute_doc(param) : param.doc,
872
+ indentation:,
873
+ )
874
+ end
875
+ end
876
+
877
+ private def emit_yard_option(parameter, name, type, description, indentation: '')
878
+ lines = description&.each_line(chomp: true)&.to_a || []
879
+ emit_comment(["#{indentation}@option #{parameter} [#{type}] #{name}", lines.shift].compact.join(' '))
880
+ lines.each do |line|
881
+ emit_comment("#{indentation} #{line}")
882
+ end
883
+ end
884
+
885
+ private def emit_yard_attribute(name, type, description)
886
+ emit_comment("@!attribute [rw] #{name}")
887
+ description&.each_line(chomp: true) do |line|
888
+ emit_comment(" #{line}")
889
+ end
890
+ emit_comment(" @return [#{type}]")
891
+ end
892
+
893
+ private def emit_yard_param(name, type, indentation: '')
894
+ emit_comment("#{indentation}@param [#{type}] #{name}")
895
+ end
896
+
897
+ private def emit_yard_return(type, description = nil, indentation: '')
898
+ emit_comment(["#{indentation}@return [#{type}]", description].compact.join(' '))
899
+ end
900
+
901
+ private def emit_yard_see(object)
902
+ emit_comment("@see #{object}")
903
+ end
904
+
905
+ private def emit_yard_yieldparam(name, type, indentation: '')
906
+ emit_comment("#{indentation}@yieldparam [#{type}] #{name}")
907
+ end
908
+
909
+ private def build_selector_plan
910
+ sets = @ynl.attribute_sets.values
911
+ @local_selectors = sets.to_h { [it, []] }
912
+ @external_selectors = sets.to_h { [it, []] }
913
+
914
+ @ynl.sub_messages.each_value do |sub_message|
915
+ duplicate = sub_message.formats.group_by(&:value).find { |_value, formats| formats.length > 1 }
916
+ if duplicate
917
+ raise ParseError,
918
+ "Duplicate format value #{duplicate.first.inspect} in sub-message #{sub_message.name.inspect}"
919
+ end
920
+ end
921
+
922
+ changed = true
923
+ while changed
924
+ changed = false
925
+ sets.each do |set|
926
+ set.attributes.each do |attribute|
927
+ if attribute.type.is_a?(Types::SubMessage)
928
+ changed |= require_selector(set, attribute.type.selector)
929
+ end
930
+
931
+ child_attribute_sets(attribute.type).each do |child|
932
+ @external_selectors.fetch(child).each do |selector|
933
+ changed |= require_selector(set, selector)
934
+ end
935
+ end
936
+ end
937
+ end
938
+ end
939
+
940
+ build_attribute_orders
941
+ roots = @ynl.operations.values.filter_map(&:attribute_set).uniq
942
+ roots.each do |root|
943
+ required = @external_selectors.fetch(root)
944
+ next if required.empty?
945
+ raise ParseError,
946
+ "Root attribute set #{root.name.inspect} requires external selectors: #{required.join(', ')}"
947
+ end
948
+ build_selector_sources(roots)
949
+ validate_selector_values
950
+ end
951
+
952
+ private def require_selector(set, selector)
953
+ local = attribute_by_name(set, selector)
954
+ if local&.multi?
955
+ raise ParseError,
956
+ "Multi-attribute #{selector.inspect} cannot be a selector in attribute set #{set.name.inspect}"
957
+ end
958
+ target = local ? @local_selectors : @external_selectors
959
+ selectors = target.fetch(set)
960
+ return false if selectors.include?(selector)
961
+ selectors << selector
962
+ true
963
+ end
964
+
965
+ private def build_attribute_orders
966
+ @attribute_orders = {}
967
+ @ynl.attribute_sets.each_value do |set|
968
+ dependencies = set.attributes.to_h { |attribute| [attribute, []] }
969
+ set.attributes.each do |attribute|
970
+ selectors = []
971
+ selectors << attribute.type.selector if attribute.type.is_a?(Types::SubMessage)
972
+ child_attribute_sets(attribute.type).each do |child|
973
+ selectors.concat(@external_selectors.fetch(child))
974
+ end
975
+
976
+ selectors.uniq.each do |selector|
977
+ selector_attribute = attribute_by_name(set, selector)
978
+ dependencies.fetch(attribute) << selector_attribute if selector_attribute
979
+ end
980
+ end
981
+
982
+ ordered = []
983
+ remaining = set.attributes.dup
984
+ until remaining.empty?
985
+ index = remaining.index do |attribute|
986
+ dependencies.fetch(attribute).all? { ordered.include?(it) }
987
+ end
988
+ unless index
989
+ raise ParseError,
990
+ "Selector dependency cycle in attribute set #{set.name.inspect}: #{remaining.map(&:name).join(', ')}"
991
+ end
992
+ ordered << remaining.delete_at(index)
993
+ end
994
+ @attribute_orders[set] = ordered.each_with_index.to_h
995
+ end
996
+ end
997
+
998
+ private def build_selector_sources(roots)
999
+ @selector_sources = @ynl.attribute_sets.values.to_h { [it, Hash.new { |h, k| h[k] = [] }] }
1000
+ reached = roots.to_h { [it, true] }
1001
+
1002
+ changed = true
1003
+ while changed
1004
+ changed = false
1005
+ reached.keys.each do |set|
1006
+ set.attributes.each do |attribute|
1007
+ child_attribute_sets(attribute.type).each do |child|
1008
+ unless reached[child]
1009
+ reached[child] = true
1010
+ changed = true
1011
+ end
1012
+ @external_selectors.fetch(child).each do |selector|
1013
+ sources = if source = attribute_by_name(set, selector)
1014
+ [source]
1015
+ else
1016
+ @selector_sources.fetch(set)[selector]
1017
+ end
1018
+ target = @selector_sources.fetch(child)[selector]
1019
+ sources.each do |candidate|
1020
+ unless target.include?(candidate)
1021
+ target << candidate
1022
+ changed = true
1023
+ end
1024
+ end
1025
+ end
1026
+ end
1027
+ end
1028
+ end
1029
+ end
1030
+ end
1031
+
1032
+ private def validate_selector_values
1033
+ @ynl.attribute_sets.each_value do |set|
1034
+ set.attributes.each do |attribute|
1035
+ next unless attribute.type.is_a?(Types::SubMessage)
1036
+ keys = attribute.type.sub_message.formats.map do |format|
1037
+ selector_value_literal(set, attribute.type.selector, format.value)
1038
+ end
1039
+ duplicate = keys.group_by(&:itself).find { |_key, matches| matches.length > 1 }
1040
+ if duplicate
1041
+ raise ParseError,
1042
+ "Sub-message #{attribute.type.sub_message.name.inspect} has duplicate compiled selector value #{duplicate.first}"
1043
+ end
1044
+ end
1045
+ end
1046
+ end
1047
+
1048
+ private def child_attribute_sets(type)
1049
+ case type
1050
+ when Types::NestedAttributes, Types::NestTypeValue
1051
+ [type.attribute_set]
1052
+ when Types::IndexedArray
1053
+ child_attribute_sets(type.sub_type)
1054
+ when Types::SubMessage
1055
+ type.sub_message.formats.filter_map(&:attribute_set)
1056
+ else
1057
+ []
1058
+ end
1059
+ end
1060
+
1061
+ private def attribute_by_name(set, name)
1062
+ set.attributes.find { it.name == name }
1063
+ end
1064
+
1065
+ private def selector_source_literal(set, selector)
1066
+ if (index = @local_selectors.fetch(set).index(selector))
1067
+ "::Nl::Selector::Local.new(#{index})"
1068
+ elsif (index = @external_selectors.fetch(set).index(selector))
1069
+ "::Nl::Selector::External.new(#{index})"
1070
+ else
1071
+ raise ParseError, "Unresolved selector #{selector.inspect} in attribute set #{set.name.inspect}"
1072
+ end
1073
+ end
1074
+
1075
+ private def selector_bindings_literal(parent, child)
1076
+ return '[]' unless child
1077
+ bindings = @external_selectors.fetch(child).map do |selector|
1078
+ selector_source_literal(parent, selector)
1079
+ end
1080
+ "Ractor.make_shareable([#{bindings.join(', ')}])"
1081
+ end
1082
+
1083
+ private def selector_value_literal(set, selector, format_value)
1084
+ sources = if source = attribute_by_name(set, selector)
1085
+ [source]
1086
+ else
1087
+ @selector_sources.fetch(set)[selector]
1088
+ end
1089
+ if sources.empty?
1090
+ raise ParseError,
1091
+ "Cannot determine the type of external selector #{selector.inspect} in attribute set #{set.name.inspect}"
1092
+ end
1093
+
1094
+ values = sources.map do |source|
1095
+ case source.type
1096
+ when Types::String
1097
+ format_value.as_string_literal
1098
+ when Types::Scalar
1099
+ unless source.enum.is_a?(Models::Enum)
1100
+ raise ParseError,
1101
+ "Integer selector #{source.name.inspect} must reference an enum"
1102
+ end
1103
+ entry = source.enum.entries.find { it.name == format_value }
1104
+ unless entry
1105
+ raise ParseError,
1106
+ "Selector enum #{source.enum.name.inspect} has no entry #{format_value.inspect}"
1107
+ end
1108
+ entry.value.to_s
1109
+ else
1110
+ raise ParseError,
1111
+ "Selector #{source.name.inspect} must be a string or an enum-backed integer"
1112
+ end
1113
+ end.uniq
1114
+ if values.length != 1
1115
+ raise ParseError,
1116
+ "Selector #{selector.inspect} has incompatible definitions for format #{format_value.inspect}"
1117
+ end
1118
+ values.first
1119
+ end
1120
+
1121
+ private def to_datatype(type, checks, owner: nil)
321
1122
  case type
322
1123
  when Types::Pad
323
- "#{@protocol}::DataTypes::Pad.new(#{type.length})"
1124
+ "::Nl::DataTypes::Pad.new(#{type.length})"
324
1125
  when Types::Flag
325
- "#{@protocol}::DataTypes::Flag.new"
1126
+ "::Nl::DataTypes::Flag.new"
326
1127
  when Types::Bitfield32
327
- "#{@protocol}::DataTypes::Bitfield32.new"
1128
+ "::Nl::DataTypes::Bitfield32.new"
328
1129
  when Types::Scalar
329
- "#{@protocol}::DataTypes::Scalar.new(::Nl::Endian::#{type.byte_order.name.as_class_name}::#{type.type.as_const_name}, check: #{to_checks(checks)})"
1130
+ byte_order = "::Nl::Endian::#{type.byte_order.name.as_class_name}"
1131
+ if ['sint', 'uint'].include?(type.type)
1132
+ "::Nl::DataTypes::VariableInteger.new(#{byte_order}, signed: #{type.type == 'sint'}, check: #{to_checks(checks)})"
1133
+ else
1134
+ "::Nl::DataTypes::Scalar.new(#{byte_order}::#{type.type.as_const_name}, check: #{to_checks(checks)})"
1135
+ end
330
1136
  when Types::String
331
- "#{@protocol}::DataTypes::String.new(check: #{to_checks(checks)})"
1137
+ "::Nl::DataTypes::String.new(check: #{to_checks(checks)})"
332
1138
  when Types::Binary
333
- # if type.struct
334
- # "Structs::" + type.struct.name.as_class_name
335
- # else
336
- "#{@protocol}::DataTypes::Binary.new(check: #{to_checks(checks)})"
337
- # end
1139
+ if type.struct
1140
+ "::Nl::DataTypes::Struct.new(Structs::#{type.struct.name.as_class_name}, check: #{to_checks(checks)}, consume_remaining: true)"
1141
+ else
1142
+ "::Nl::DataTypes::Binary.new(check: #{to_checks(checks)})"
1143
+ end
1144
+ when Types::PackedArray
1145
+ "::Nl::DataTypes::PackedArray.new(#{to_datatype(type.sub_type, nil)}, check: #{to_checks(checks)})"
338
1146
  when Types::NestedAttributes
339
- "#{@protocol}::DataTypes::NestedAttributes.new(#{type.attribute_set.name.as_class_name})"
1147
+ bindings = selector_bindings_literal(owner, type.attribute_set)
1148
+ "::Nl::DataTypes::NestedAttributes.new(AttributeSets::#{type.attribute_set.name.as_class_name}, selector_bindings: #{bindings})"
1149
+ when Types::NestTypeValue
1150
+ bindings = selector_bindings_literal(owner, type.attribute_set)
1151
+ "::Nl::DataTypes::NestTypeValue.new(AttributeSets::#{type.attribute_set.name.as_class_name}, #{type.type_values.length}, selector_bindings: #{bindings})"
340
1152
  when Types::IndexedArray
341
- "#{@protocol}::DataTypes::IndexedArray.new(#{to_datatype(type.sub_type, nil)})"
1153
+ "::Nl::DataTypes::IndexedArray.new(#{to_datatype(type.sub_type, nil, owner:)})"
342
1154
  when Types::SubMessage
343
- "#{@protocol}::DataTypes::Binary.new(check: nil)"
1155
+ selector = selector_source_literal(owner, type.selector)
1156
+ formats = type.sub_message.formats.map do |format|
1157
+ key = selector_value_literal(owner, type.selector, format.value)
1158
+ klass = "SubMessages::#{type.sub_message.name.as_class_name}::#{format.value.as_class_name}"
1159
+ bindings = selector_bindings_literal(owner, format.attribute_set)
1160
+ nested = !format.attribute_set.nil?
1161
+ "#{key} => ::Nl::DataTypes::SubMessage::Format.new(#{klass}, #{bindings}, #{nested})"
1162
+ end
1163
+ "::Nl::DataTypes::SubMessage.new(#{selector}, {#{formats.join(', ')}})"
344
1164
  else
345
1165
  raise "Unknown type: #{type.class}"
346
1166
  end
347
1167
  end
348
1168
 
1169
+ private def to_struct_member_datatype(type)
1170
+ if type.is_a?(Types::Binary) && type.struct
1171
+ "::Nl::DataTypes::Struct.new(Structs::#{type.struct.name.as_class_name}, check: nil)"
1172
+ elsif type.is_a?(Types::Binary)
1173
+ "::Nl::DataTypes::Binary.new(length: #{type.length}, check: nil)"
1174
+ else
1175
+ to_datatype(type, nil)
1176
+ end
1177
+ end
1178
+
349
1179
  private def to_checks(checks)
350
1180
  return 'nil' if !checks || checks.empty?
351
- %Q{-> { #{checks.join('; ')} }}
1181
+ %Q{-> { #{checks.map { to_check(it) }.join('; ')} }}
1182
+ end
1183
+
1184
+ private def to_check(check)
1185
+ value = case check.value
1186
+ when Models::Const
1187
+ "Constants::#{check.value.name.as_const_name}"
1188
+ else
1189
+ integer_literal(check.value)
1190
+ end
1191
+
1192
+ comparison, description = case check.operation
1193
+ when 'max'
1194
+ ["it <= #{value}", "greater than maximum #{check_message_value(check, value)}"]
1195
+ when 'min'
1196
+ ["it >= #{value}", "less than minimum #{check_message_value(check, value)}"]
1197
+ when 'min-len'
1198
+ ["it.bytesize >= #{value}", "shorter than minimum length #{check_message_value(check, value)}"]
1199
+ when 'max-len'
1200
+ ["it.bytesize <= #{value}", "longer than maximum length #{check_message_value(check, value)}"]
1201
+ when 'exact-len'
1202
+ ["it.bytesize == #{value}", "not equal to length #{check_message_value(check, value)}"]
1203
+ else
1204
+ raise "Unknown check: #{check.operation}"
1205
+ end
1206
+
1207
+ %Q{raise ArgumentError, "Value \#{it.inspect} is #{description}" unless #{comparison}}
1208
+ end
1209
+
1210
+ private def check_message_value(check, value)
1211
+ check.value.is_a?(Models::Const) ? "\#{#{value}}" : value
1212
+ end
1213
+
1214
+ private def const_value_literal(value)
1215
+ case value
1216
+ when Integer
1217
+ value.to_s
1218
+ when String
1219
+ value.as_string_literal
1220
+ else
1221
+ raise ParseError, "YNL constant value must be a string or an integer, got #{value.class}"
1222
+ end
1223
+ end
1224
+
1225
+ private def integer_literal(value)
1226
+ unless value.is_a?(Integer)
1227
+ raise ParseError, "YNL check value must be an integer, got #{value.class}"
1228
+ end
1229
+
1230
+ value.to_s
1231
+ end
1232
+
1233
+ private def mcast_group_value_literal(value)
1234
+ case value
1235
+ when Integer
1236
+ value.to_s
1237
+ when nil
1238
+ 'nil'
1239
+ else
1240
+ raise ParseError, "YNL multicast group value must be an integer, got #{value.class}"
1241
+ end
352
1242
  end
353
1243
  end
354
1244
  end