ynl 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b6a0856ea9a2918cc0a1dee7d9452227091209776df336059a4eae4ca85b903e
4
- data.tar.gz: 4513210c607ad814734cc0ed03f5e57ed1853da3b86539015a933db6364c5205
3
+ metadata.gz: a85c9c15709c41d276a7a6f464fa8c1268e90d4e9513534bc631fd4a2d1353d8
4
+ data.tar.gz: 9ad5798aa23e2bdc04a530a1efa5b45dd1961b3fa2972adfe8efa5333874ecf7
5
5
  SHA512:
6
- metadata.gz: ba1fd6ff654ad9c613e98ddcbdf9e28e3b9b24eac873f5e1c19aa2cfa919e2020bc9c7557691d235554f72cc279f89e4612027aef1feaeda43c3d9a98201f76d
7
- data.tar.gz: 39ca59b427aa8d8c945350a351518063407583e7e511acf0e4ed5ba901b860dbab65b32daaf0a1218ccdbf7fddd6d0a5c826761f80393f88b361159a8fee4be9
6
+ metadata.gz: 78776aab24ab1126e2d5c2f8b8e09f98bd55004c123dc40f3c52db5bc113ee573746e8f9f45b07309992755c8de7bdd373617226645b64c3ad8b70a4fb10be44
7
+ data.tar.gz: 86a920b0c09e38515ecccd413f73b7cc8a324359875df33f30eebeb883d0f55479ec235897efea33c9d309741065ac2e249500fce849501da2a5c2608e4cfdd2
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # ChangeLog
2
+
3
+ ## Unreleased
4
+
5
+ ## v0.2.0 (2026-03-02)
6
+
7
+ ## v0.1.0 (2025-04-20)
8
+
9
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Kasumi Hanazuki
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # ynl
2
+
3
+ YNL (YAML Netlink Specification) parser and code generator.
data/Rakefile CHANGED
@@ -1,4 +1,3 @@
1
- require 'bundler/gem_tasks'
2
1
  require 'rspec/core/rake_task'
3
2
 
4
3
  RSpec::Core::RakeTask.new(:spec)
data/lib/ynl/family.rb CHANGED
@@ -5,16 +5,18 @@ require_relative 'generator'
5
5
 
6
6
  module Ynl
7
7
  class Family
8
+ # Builds a Ruby class from a spec file
8
9
  def self.build(path)
9
- require 'nl'
10
-
11
- defs = Ynl::Parser.parse_file(path)
12
-
13
10
  buf = StringIO.new
14
- classname = Generator.new(defs, buf).generate(namespace: 'self')
15
- classdef = buf.string
11
+ classname = File.open(path) {|f| generate(f, buf, namespace: 'self') }
12
+
13
+ Module.new { eval(buf.string) }.const_get(classname)
14
+ end
16
15
 
17
- Module.new { eval(classdef) }.const_get(classname)
16
+ # Generates Ruby code from a spec source
17
+ def self.generate(source, out, **kwargs)
18
+ out << Generator::PRELUDE
19
+ Generator.new(Ynl::Parser.new(source).parse, out).generate(**kwargs)
18
20
  end
19
21
  end
20
22
  end
data/lib/ynl/generator.rb CHANGED
@@ -5,15 +5,15 @@ module Ynl
5
5
 
6
6
  refine(String) do
7
7
  def as_const_name
8
- split(WORD_DELIM).map(&:upcase).join('_')
8
+ split(WORD_DELIM).map(&:upcase).join('_').tap {|s| s.prepend('X_') unless s.start_with?(/[A-Z]/i) }
9
9
  end
10
10
 
11
11
  def as_class_name
12
- split(WORD_DELIM).map(&:capitalize).join
12
+ split(WORD_DELIM).map(&:capitalize).join.tap {|s| s.prepend('X') unless s.start_with?(/[A-Z]/i) }
13
13
  end
14
14
 
15
15
  def as_variable_name
16
- split(WORD_DELIM).join('_')
16
+ split(WORD_DELIM).join('_').tap {|s| s.prepend('x_') unless s.start_with?(/[A-Z]/i) }
17
17
  end
18
18
  alias as_method_name as_variable_name
19
19
 
@@ -26,10 +26,17 @@ module Ynl
26
26
  end
27
27
  end
28
28
  end
29
- private_constant :Refinements
29
+ # private_constant :Refinements
30
30
 
31
31
  using Refinements
32
32
 
33
+ PRELUDE = -<<~RUBY
34
+ # frozen_string_literal: true
35
+ # rbs_inline: enabled
36
+ # This code is generated by Ynl::Generator. DO NOT EDIT.
37
+ require 'nl'
38
+ RUBY
39
+
33
40
  def initialize(ynl, out)
34
41
  @ynl = ynl
35
42
  @out = out
@@ -37,29 +44,54 @@ module Ynl
37
44
  end
38
45
 
39
46
  def generate(superclass: '::Nl::Family', namespace: nil)
47
+ @protocol = '::Nl::Protocols::' + (@ynl.protocol == 'netlink-raw' ? 'Raw' : 'Genl')
48
+
49
+ emit_comment(@ynl.doc)
50
+
40
51
  classname = @ynl.name.as_class_name
41
52
  emit_class([*namespace, classname].join('::'), superclass) do
42
- write('NAME = ', @ynl.name.as_string_literal)
43
- write('PROTONUM = ', @ynl.protonum)
53
+ emit_const('NAME', @ynl.name.as_string_literal)
44
54
 
45
55
  if @ynl.protocol == 'netlink-raw'
46
- write('PROTOCOL = Ractor.make_shareable(::Nl::Protocols::Raw.new(', @ynl.name.as_string_literal, ', ', @ynl.protonum ,'))')
56
+ emit_const('PROTOCOL', "Ractor.make_shareable(::Nl::Protocols::Raw.new(#{@ynl.name.as_string_literal}, #{@ynl.protonum}))")
47
57
  else
48
- write('PROTOCOL = Ractor.make_shareable(::Nl::Protocols::Genl.new(', @ynl.name.as_string_literal, ')')
58
+ emit_const('PROTOCOL', "Ractor.make_shareable(::Nl::Protocols::Genl.new(#{@ynl.name.as_string_literal}))")
49
59
  end
50
60
 
51
61
  emit_module('Structs') do
52
62
  @ynl.structs.each do |name, struct|
53
63
  emit_comment(struct.doc)
54
- write(name.as_class_name, ' = Struct.new(', struct.members.map { it.name.as_variable_name.as_symbol_literal }.join(', ') ,')')
64
+ write(name.as_class_name, " = Struct.new(")
65
+ indent do
66
+ struct.members.each do |member|
67
+ write(member.name.as_variable_name.as_symbol_literal, ', #: ', member.type.rbs_type)
68
+ end
69
+ end
70
+ write(')')
55
71
  emit_class(name.as_class_name) do
56
- write('MEMBERS = Ractor.make_shareable({', struct.members.map { "#{it.name.as_variable_name}: #{to_datatype(it.type, nil)}" }.join(', ') ,'})')
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
+ )
78
+
79
+ emit_comment('Decodes the struct.')
80
+ emit_rbs_comment(
81
+ 'decoder: ::Nl::Decoder',
82
+ 'return: instance',
83
+ )
57
84
  write('def self.decode(decoder)')
58
85
  indent do
59
86
  write('self.new(*MEMBERS.map {|name, datatype| datatype.decode(decoder) })')
60
87
  end
61
88
  write('end')
62
89
 
90
+ emit_comment('Encodes the struct.')
91
+ emit_rbs_comment(
92
+ 'encoder: ::Nl::Encoder',
93
+ 'return: void',
94
+ )
63
95
  write('def encode(encoder)')
64
96
  indent do
65
97
  write('MEMBERS.each {|name, datatype| datatype.encode(encoder, self.public_send(name)) }')
@@ -70,56 +102,106 @@ module Ynl
70
102
  end
71
103
 
72
104
  emit_module('AttributeSets') do
105
+ deferred_consts = []
73
106
  @ynl.attribute_sets.each do |name, attr_set|
74
107
  emit_comment(attr_set.doc)
75
- emit_class(name.as_class_name, '::Nl::Family::AttributeSet') do
108
+ emit_class(name.as_class_name, @protocol + '::AttributeSet') do
76
109
  emit_comment("Abstract class")
77
- emit_class('Attribute', '::Nl::Family::AttributeSet::Attribute') do
110
+ emit_class('Attribute', @protocol + '::AttributeSet::Attribute') do
78
111
  end
79
112
  attr_set.attributes.each do |attr|
80
113
  emit_comment(attr.doc)
81
114
  emit_class(attr.name.as_class_name, 'Attribute') do
82
- write('TYPE = ', attr.value)
83
- write('NAME =', attr.name.as_variable_name.as_symbol_literal)
84
- write('DATATYPE = ', to_datatype(attr.type, attr.checks))
115
+ emit_const('TYPE', attr.value)
116
+ emit_const('NAME', attr.name.as_variable_name.as_symbol_literal)
117
+ if attr.type.is_a?(Types::NestedAttributes) ||
118
+ (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)]
120
+ else
121
+ emit_const('DATATYPE', to_datatype(attr.type, attr.checks))
122
+ end
85
123
  end
86
124
  end
87
125
 
88
- write('BY_NAME = Ractor.make_shareable({', attr_set.attributes.map { "#{it.name.as_variable_name.as_symbol_literal} => #{it.name.as_class_name}" }.join(', ') ,'})')
89
- write('BY_TYPE = Ractor.make_shareable({', attr_set.attributes.map { "#{it.value} => #{it.name.as_class_name}" }.join(', ') ,'})')
126
+ emit_nodoc
127
+ emit_const(
128
+ 'BY_NAME',
129
+ "Ractor.make_shareable({#{attr_set.attributes.map { "#{it.name.as_variable_name.as_symbol_literal} => #{it.name.as_class_name}" }.join(', ') }})",
130
+ rbs: 'Hash[::Symbol, Attribute]',
131
+ )
132
+
133
+ emit_nodoc
134
+ emit_const(
135
+ 'BY_TYPE',
136
+ "Ractor.make_shareable({#{attr_set.attributes.map { "#{it.value} => #{it.name.as_class_name}" }.join(', ') }})",
137
+ rbs: 'Hash[::Integer, Attribute]',
138
+ )
90
139
 
91
140
  emit_singleton_class do
141
+ emit_comment('Looks up Attribute class by name.')
92
142
  emit_rbs_comment(
93
143
  'name: Symbol',
94
144
  'return: Attribute',
95
145
  )
96
- write('def by_name(name) = BY_NAME[name]')
146
+ emit_getter('by_name(name)', 'BY_NAME.fetch(name)')
97
147
 
148
+ emit_comment('Looks up Attribute class by type value.')
98
149
  emit_rbs_comment(
99
150
  'type: Integer',
100
151
  'return: Attribute',
101
152
  )
102
- write('def by_type(type) = BY_TYPE[type]')
153
+ emit_getter('by_type(type)', 'BY_TYPE.fetch(type)')
103
154
  end
104
155
  end
105
156
  end
157
+ deferred_consts.each { emit_const(*it) }
106
158
  end
107
159
 
108
160
  emit_module('Messages') do
109
161
  @ynl.operations.each do |name, oper|
110
- emit_comment(oper.doc)
111
162
  %w[do dump].each do |method|
112
163
  if request_reply = oper.public_send(method + 'it')
113
164
  %w[request reply].to_h { [it, request_reply.public_send(it)] }.compact.each do |type, msg|
114
- emit_class(method.as_class_name + oper.name.as_class_name + type.as_class_name, '::Nl::Family::Message') do
115
- write('TYPE = ', msg.value)
116
- write('FIXED_HEADER = ', oper.fixed_header&.then { 'Structs::' + it.name.as_class_name } || 'nil')
117
- write('ATTRIBUTE_SET = AttributeSets::', oper.attribute_set.name.as_class_name)
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}")
118
170
  params = msg.attributes
119
- header_params = params & (oper.fixed_header&.members&.map(&:name) || [])
120
- attribute_params = params & (oper.attribute_set.attributes.map(&:name))
121
- write('HEADER_PARAMS = Ractor.make_shareable(%i[', header_params.map { it.as_variable_name }.join(' ') ,'])')
122
- write('ATTRIBUTE_PARAMS = Ractor.make_shareable(%i[', attribute_params.map { it.as_variable_name }.join(' ') ,'])')
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
123
205
  end
124
206
  end
125
207
  end
@@ -131,16 +213,37 @@ module Ynl
131
213
  @ynl.operations.each do |name, oper|
132
214
  %w[do dump].each do |method|
133
215
  if request_reply = oper.public_send(method + 'it')
216
+ next unless request = request_reply.request # FIXME: what should we do in this case?
217
+
218
+ request_class = "Messages::#{method.as_class_name}#{oper.name.as_class_name}Request"
219
+ reply_class = request_reply.reply ? "Messages::#{method.as_class_name}#{oper.name.as_class_name}Reply" : 'nil'
220
+
221
+ header_params = oper.fixed_header&.members || []
222
+ attribute_params = oper.attribute_set.attributes.filter { request.attributes.include?(it.name) }
223
+ attribute_params.reject! {|a| header_params.any? {|h| h.name == a.name } } # The two params should have the same Ruby type anyway
224
+ params = header_params + attribute_params
225
+ params.reject! { it.type.is_a? Types::Pad }
226
+
227
+ rbs_params = params.map { |it| "?#{it.name.as_variable_name}: #{it.type.rbs_type}" }.join(', ')
228
+ rbs_result = request_reply.reply ? reply_class : 'void'
229
+
134
230
  emit_comment(oper.doc)
135
- write('def ', method.as_method_name, '_', oper.name.as_method_name, '(**args)')
136
- indent do
137
- request_class = "Messages::#{method.as_class_name}#{oper.name.as_class_name}Request"
138
- if request_reply.reply
139
- reply_class = "Messages::#{method.as_class_name}#{oper.name.as_class_name}Reply"
140
- else
141
- reply_class = 'nil'
231
+ if method == 'dump'
232
+ emit_rbs_comment(
233
+ "(#{rbs_params}) -> Enumerable[#{rbs_result}]\n | (#{rbs_params}) { (#{rbs_result}) -> void } -> void",
234
+ )
235
+ write("def #{method.as_method_name}_#{oper.name.as_method_name}(**args, &block)")
236
+ indent do
237
+ write("exchange_message(#{method.as_symbol_literal}, #{request_class}, #{reply_class}, args, &block)")
238
+ end
239
+ else
240
+ emit_rbs_comment(
241
+ "(#{rbs_params}) -> #{rbs_result}",
242
+ )
243
+ write("def #{method.as_method_name}_#{oper.name.as_method_name}(**args)")
244
+ indent do
245
+ write("exchange_message(#{method.as_symbol_literal}, #{request_class}, #{reply_class}, args)")
142
246
  end
143
- write("exchange_message(#{method.as_symbol_literal}, #{request_class}, #{reply_class}, args)")
144
247
  end
145
248
  write('end')
146
249
  end
@@ -188,35 +291,56 @@ module Ynl
188
291
  write('end')
189
292
  end
190
293
 
294
+ private def emit_const(name, value, rbs: nil)
295
+ write(name, ' = ', value, *([' #: ', rbs] if rbs))
296
+ end
297
+
298
+ private def emit_getter(name, expr)
299
+ write('def ', name, '; ', expr, '; end')
300
+ end
301
+
302
+ private def emit_nodoc
303
+ write('# :nodoc:')
304
+ end
305
+
191
306
  private def emit_comment(comment)
192
307
  return unless comment
193
- comment.each_line do |line|
308
+ comment.each_line(chomp: true) do |line|
194
309
  write('# ', line)
195
310
  end
196
311
  end
197
312
 
198
313
  private def emit_rbs_comment(*args)
314
+ write('#--')
199
315
  args.each do |arg|
200
- write('# @rbs ', arg)
316
+ emit_comment('@rbs ' + arg)
201
317
  end
202
318
  end
203
319
 
204
320
  private def to_datatype(type, checks)
205
321
  case type
206
322
  when Types::Pad
207
- 'nil'
323
+ "#{@protocol}::DataTypes::Pad.new(#{type.length})"
324
+ when Types::Flag
325
+ "#{@protocol}::DataTypes::Flag.new"
326
+ when Types::Bitfield32
327
+ "#{@protocol}::DataTypes::Bitfield32.new"
208
328
  when Types::Scalar
209
- "PROTOCOL.class::DataTypes::Scalar.new(::Nl::Endian::#{type.byte_order.name.as_class_name}::#{type.type.as_class_name}, check: #{to_checks(checks)})"
329
+ "#{@protocol}::DataTypes::Scalar.new(::Nl::Endian::#{type.byte_order.name.as_class_name}::#{type.type.as_const_name}, check: #{to_checks(checks)})"
210
330
  when Types::String
211
- "PROTOCOL.class::DataTypes::String.new(check: #{to_checks(checks)})"
331
+ "#{@protocol}::DataTypes::String.new(check: #{to_checks(checks)})"
212
332
  when Types::Binary
213
333
  # if type.struct
214
334
  # "Structs::" + type.struct.name.as_class_name
215
335
  # else
216
- "PROTOCOL.class::DataTypes::Binary.new(check: #{to_checks(checks)})"
336
+ "#{@protocol}::DataTypes::Binary.new(check: #{to_checks(checks)})"
217
337
  # end
218
338
  when Types::NestedAttributes
219
- "PROTOCOL.class::DataTypes::NestedAttributes.new(#{type.attribute_set.name.as_class_name})"
339
+ "#{@protocol}::DataTypes::NestedAttributes.new(#{type.attribute_set.name.as_class_name})"
340
+ when Types::IndexedArray
341
+ "#{@protocol}::DataTypes::IndexedArray.new(#{to_datatype(type.sub_type, nil)})"
342
+ when Types::SubMessage
343
+ "#{@protocol}::DataTypes::Binary.new(check: nil)"
220
344
  else
221
345
  raise "Unknown type: #{type.class}"
222
346
  end
data/lib/ynl/models.rb CHANGED
@@ -27,6 +27,23 @@ module Ynl
27
27
  end
28
28
  end
29
29
 
30
+ Const = ::Struct.new(:name, :value, :doc)
31
+
32
+ class SubMessage
33
+ Format = ::Struct.new(:value, :attribute_set)
34
+
35
+ attr_reader :name, :formats
36
+
37
+ def initialize(name:)
38
+ @name = name
39
+ @formats = []
40
+ end
41
+
42
+ def resolve(f)
43
+ self
44
+ end
45
+ end
46
+
30
47
  class Enum
31
48
  Entry = ::Struct.new(:name, :value, :doc)
32
49
 
@@ -106,6 +123,31 @@ module Ynl
106
123
  end
107
124
  end
108
125
 
126
+ class AttributeSubset
127
+ attr_reader :name, :superset, :doc
128
+
129
+ def initialize(name:, superset:, attributes:, doc:)
130
+ @name = name
131
+ @superset = superset
132
+ @attributes = attributes
133
+ @doc = doc
134
+ end
135
+
136
+ 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
144
+ end
145
+
146
+ def resolve(f)
147
+ self
148
+ end
149
+ end
150
+
109
151
  class Operation
110
152
  attr_reader :name, :doc, :fixed_header, :attribute_set, :doit, :dumpit
111
153
 
@@ -119,8 +161,8 @@ module Ynl
119
161
  end
120
162
 
121
163
  def resolve(f)
122
- @doit = @doit.resolve(f)
123
- @dumpit = @dumpit.resolve(f)
164
+ @doit = @doit&.resolve(f)
165
+ @dumpit = @dumpit&.resolve(f)
124
166
  self
125
167
  end
126
168
  end
@@ -134,8 +176,8 @@ module Ynl
134
176
  end
135
177
 
136
178
  def resolve(f)
137
- @request = @request.resolve(f)
138
- @reply = @reply.resolve(f)
179
+ @request = @request&.resolve(f)
180
+ @reply = @reply&.resolve(f)
139
181
  self
140
182
  end
141
183
  end
data/lib/ynl/parser.rb CHANGED
@@ -26,7 +26,7 @@ module Ynl
26
26
  protocol = @yaml['protocol'] || 'genetlink'
27
27
  protonum = @yaml['protonum']
28
28
  name = @yaml['name']
29
- doc = @yaml['doc']
29
+ doc = translate_doc(@yaml['doc'])
30
30
 
31
31
  @yaml['definitions']&.each do |d|
32
32
  parse_definition(d)
@@ -41,14 +41,42 @@ module Ynl
41
41
  enum_model = case operations['enum-model']
42
42
  when 'directional'
43
43
  :directional
44
- when nil
45
- :unidirectional
44
+ when 'unified', nil
45
+ :unified
46
46
  else
47
47
  raise ParseError, "Unknown enum model: #{operations['enum-model']}"
48
48
  end
49
49
  @default_fixed_header = operations['fixed-header']&.then { @structs.fetch(it) }
50
- operations['list']&.each do |d|
51
- parse_operation(d, enum_model:)
50
+
51
+ if enum_model == :unified
52
+ counter = 0
53
+ operations['list']&.each do |d|
54
+ counter = d['value'] || (counter + 1)
55
+ parse_operation(d, request_id: counter, reply_id: counter)
56
+ end
57
+ else
58
+ request_counter = 1
59
+ reply_counter = 1
60
+ operations['list']&.each do |d|
61
+ if d.key?('notify') || d.key?('event')
62
+ reply_counter = d['value'] || reply_counter
63
+ parse_operation(d, request_id: nil, reply_id: reply_counter)
64
+ reply_counter += 1
65
+ else
66
+ primary = d['do'] || d['dump']
67
+ request_counter = primary&.dig('request', 'value') || request_counter
68
+ request_id = request_counter
69
+ request_counter += 1
70
+ if primary&.key?('reply')
71
+ reply_counter = primary.dig('reply', 'value') || reply_counter
72
+ reply_id = reply_counter
73
+ reply_counter += 1
74
+ else
75
+ reply_id = nil
76
+ end
77
+ parse_operation(d, request_id:, reply_id:)
78
+ end
79
+ end
52
80
  end
53
81
  end
54
82
  @yaml['mcast-groups']&.each do |d|
@@ -90,9 +118,14 @@ module Ynl
90
118
  end
91
119
  end
92
120
 
121
+ private def parse_const(d)
122
+ name = "#{d['name-prefix']}#{d.fetch('name')}"
123
+ Models::Const.new(name, d.fetch('value'), translate_doc(d['doc']))
124
+ end
125
+
93
126
  private def parse_enum_flags(d, type:)
94
127
  cls = type == :enum ? Models::Enum : Models::Flags
95
- result = cls.new(name: d.fetch('name'), doc: d['doc'])
128
+ result = cls.new(name: d.fetch('name'), doc: translate_doc(d['doc']))
96
129
 
97
130
  start_value = d['start-value'] || 0
98
131
  value = type == :enum ? start_value : 1 << start_value
@@ -102,7 +135,7 @@ module Ynl
102
135
  when String
103
136
  entry = cls::Entry.new(name: v, value:)
104
137
  when Hash
105
- entry = cls::Entry.new(name: v.fetch('name'), value:, doc: v['doc'])
138
+ entry = cls::Entry.new(name: v.fetch('name'), value:, doc: translate_doc(v['doc']))
106
139
  else
107
140
  raise ParseError, "Unknown class for enum/flags entry: #{v.class}"
108
141
  end
@@ -145,7 +178,7 @@ module Ynl
145
178
  private def parse_attribute_type(d)
146
179
  type = d.fetch('type')
147
180
  case type
148
- when 'u8', 'u16', 'u32', 'u64', 's8', 's16', 's32', 's64', 'int', 'uint'
181
+ when 'u8', 'u16', 'u32', 'u64', 's8', 's16', 's32', 's64', 'int', 'uint', 'sint'
149
182
  Types::Scalar.new(
150
183
  type: type,
151
184
  byte_order: parse_byte_order(d['byte-order']),
@@ -161,6 +194,18 @@ module Ynl
161
194
  Types::NestedAttributes.new(
162
195
  attribute_set: Models::Thunk.new {|f| f.attribute_sets.fetch(d.fetch('nested-attributes')) },
163
196
  )
197
+ when 'indexed-array'
198
+ Types::IndexedArray.new(
199
+ sub_type: parse_indexed_array_sub_type(d),
200
+ )
201
+ when 'nest-type-value'
202
+ if d['nested-attributes']
203
+ Types::NestedAttributes.new(
204
+ attribute_set: Models::Thunk.new {|f| f.attribute_sets.fetch(d.fetch('nested-attributes')) },
205
+ )
206
+ else
207
+ Types::Binary.new(struct: nil, display_hint: nil)
208
+ end
164
209
  when 'sub-message'
165
210
  Types::SubMessage.new(
166
211
  sub_message: Models::Thunk.new {|f| f.sub_messages.fetch(d.fetch('sub-message')) },
@@ -170,6 +215,10 @@ module Ynl
170
215
  Types::Pad.new(
171
216
  length: nil,
172
217
  )
218
+ when 'flag'
219
+ Types::Flag.new
220
+ when 'bitfield32'
221
+ Types::Bitfield32.new
173
222
  when 'unused'
174
223
  nil
175
224
  else
@@ -177,6 +226,28 @@ module Ynl
177
226
  end
178
227
  end
179
228
 
229
+ private def parse_indexed_array_sub_type(d)
230
+ sub_type = d.fetch('sub-type')
231
+ case sub_type
232
+ when 'u8', 'u16', 'u32', 'u64', 's8', 's16', 's32', 's64', 'int', 'uint', 'sint'
233
+ Types::Scalar.new(
234
+ type: sub_type,
235
+ byte_order: parse_byte_order(d['byte-order']),
236
+ )
237
+ when 'binary'
238
+ Types::Binary.new(
239
+ struct: nil,
240
+ display_hint: d['display-hint'],
241
+ )
242
+ when 'nest'
243
+ Types::NestedAttributes.new(
244
+ attribute_set: Models::Thunk.new {|f| f.attribute_sets.fetch(d.fetch('nested-attributes')) },
245
+ )
246
+ else
247
+ raise ParseError, "Unknown indexed-array sub-type: #{sub_type}"
248
+ end
249
+ end
250
+
180
251
  private def parse_byte_order(v)
181
252
  case v
182
253
  when nil
@@ -191,11 +262,11 @@ module Ynl
191
262
  end
192
263
 
193
264
  private def parse_struct(d)
194
- result = Models::Struct.new(name: d.fetch('name'), doc: d['doc'])
265
+ result = Models::Struct.new(name: d.fetch('name'), doc: translate_doc(d['doc']))
195
266
 
196
267
  d.fetch('members').each do |v|
197
268
  type = parse_struct_member_type(v)
198
- member = Models::Struct::Member.new(name: v.fetch('name'), type: type, doc: v['doc'])
269
+ member = Models::Struct::Member.new(name: v.fetch('name'), type: type, doc: translate_doc(v['doc']))
199
270
  result.members << member
200
271
  rescue
201
272
  raise ParseError, "Failed to parse struct member: #{v.fetch('name')}"
@@ -207,7 +278,7 @@ module Ynl
207
278
  end
208
279
 
209
280
  private def parse_checks(d)
210
- d.map do |op, value|
281
+ d.filter_map do |op, value|
211
282
  parse_check(op, value)
212
283
  end
213
284
  rescue
@@ -218,16 +289,21 @@ module Ynl
218
289
  case op
219
290
  when 'max'
220
291
  value = parse_value(value_literal)
221
- return %{raise unless it <= #{value}}
292
+ return %{raise ArgumentError, "Value \#{it.inspect} is greater than maximum #{value}" unless it <= #{value}}
222
293
  when 'min'
223
294
  value = parse_value(value_literal)
224
- return %{raise unless it >= #{value}}
295
+ return %{raise ArgumentError, "Value \#{it.inspect} is less than minimum #{value}" unless it >= #{value}}
225
296
  when 'min-len'
226
297
  value = parse_value(value_literal)
227
- return %{raise unless it.bytesize >= #{value}}
298
+ return %{raise ArgumentError, "Value \#{it.inspect} is shorter than minimum length #{value}" unless it.bytesize >= #{value}}
228
299
  when 'max-len'
229
300
  value = parse_value(value_literal)
230
- return %{raise unless it.bytesize <= #{value}}
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}}
305
+ when 'unterminated-ok'
306
+ return nil
231
307
  else
232
308
  raise ParseError, "Unknown check: #{op}"
233
309
  end
@@ -243,6 +319,9 @@ module Ynl
243
319
  (2 ** 32) - 1
244
320
  when 's32-max'
245
321
  (2 ** 31) - 1
322
+ when String
323
+ const = @consts[v] or raise ParseError, "Unknown value: #{v}"
324
+ const.value
246
325
  else
247
326
  raise ParseError, "Unknown value: #{v}"
248
327
  end
@@ -251,22 +330,11 @@ module Ynl
251
330
  private def parse_attribute_set(d)
252
331
  name = d.fetch('name')
253
332
  if subset_of = d['subset-of']
254
- result = Models::Thunk.new do |f|
255
- superset = f.attribute_sets.fetch(subset_of)
256
- attribute_set = Models::AttributeSet.new(name:, name_prefix: superset.name_prefix, doc: d['doc'])
257
- d.fetch('attributes').each do |v|
258
- aname = v.fetch('name')
259
- sattr = superset.attributes.find {|a| a.name == aname } or raise ParseError, "Attribute not found: #{aname}"
260
- # TODO: type/checks overrides
261
- attribute_set.attributes << sattr
262
- rescue
263
- raise ParseError, "Failed to parse attribute: #{v.fetch('name')}"
264
- end
265
- attribute_set
266
- end
333
+ superset = @attribute_sets.fetch(subset_of)
334
+ result = Models::AttributeSubset.new(name:, superset:, attributes: d.fetch('attributes'), doc: translate_doc(d['doc']))
267
335
  else
268
336
  name_prefix = d['name_prefix']
269
- result = Models::AttributeSet.new(name:, name_prefix:, doc: d['doc'])
337
+ result = Models::AttributeSet.new(name:, name_prefix:, doc: translate_doc(d['doc']))
270
338
  value = 0
271
339
 
272
340
  d.fetch('attributes').each do |v|
@@ -290,9 +358,16 @@ module Ynl
290
358
  end
291
359
 
292
360
  private def parse_sub_message(d)
361
+ name = d.fetch('name')
362
+ result = Models::SubMessage.new(name:)
363
+ 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)
366
+ end
367
+ @sub_messages[name] = result
293
368
  end
294
369
 
295
- private def parse_operation(d, enum_model:)
370
+ private def parse_operation(d, request_id:, reply_id:)
296
371
  name = d.fetch('name')
297
372
 
298
373
  fixed_header = d['fixed-header']&.then do
@@ -307,24 +382,35 @@ module Ynl
307
382
  raise ParseError, "Undefined attribute set: #{it}"
308
383
  end
309
384
 
310
- doit = d['do']&.then { parse_request_reply(it) }
311
- dumpit = d['dump']&.then { parse_request_reply(it) }
385
+ doit = d['do']&.then { parse_request_reply(it, default_request_id: request_id, default_reply_id: reply_id) }
386
+ dumpit = d['dump']&.then { parse_request_reply(it, default_request_id: request_id, default_reply_id: reply_id) }
387
+
388
+ # TODO: notify
312
389
 
313
- @operations[name] = Models::Operation.new(name:, doc: d['doc'], fixed_header:, attribute_set:, doit:, dumpit:)
390
+ @operations[name] = Models::Operation.new(
391
+ name:, doc: translate_doc(d['doc']), fixed_header:, attribute_set:,
392
+ doit:, dumpit:,
393
+ )
314
394
  end
315
395
 
316
- private def parse_request_reply(d)
396
+ private def parse_request_reply(d, default_request_id:, default_reply_id:)
317
397
  Models::RequestReply.new(
318
- request: d['request']&.then { parse_message(it) },
319
- reply: d['reply']&.then { parse_message(it) },
398
+ request: d['request']&.then { parse_message(it, default_id: default_request_id) },
399
+ reply: d['reply']&.then { parse_message(it, default_id: default_reply_id) },
320
400
  )
321
401
  end
322
402
 
323
- private def parse_message(d)
324
- Models::Message.new(value: d['value'], attributes: d.fetch('attributes', []))
403
+ private def parse_message(d, default_id:)
404
+ Models::Message.new(value: d['value'] || default_id, attributes: d.fetch('attributes', []))
325
405
  end
326
406
 
327
407
  private def parse_mcast_group(d)
328
408
  end
409
+
410
+ private def translate_doc(doc)
411
+ return unless doc
412
+
413
+ doc.gsub(/(?:\s|^)\K@(?<ident>[\w.-]+)/i) { "`#{$~[:ident]}`" }
414
+ end
329
415
  end
330
416
  end
@@ -0,0 +1,3 @@
1
+ module Ynl
2
+ VERSION = '0.2.0'
3
+ end
data/lib/ynl.rb CHANGED
@@ -12,36 +12,92 @@ module Ynl
12
12
  def resolve(f)
13
13
  self
14
14
  end
15
+
16
+ def rbs_type
17
+ '::Integer'
18
+ end
15
19
  end
16
20
  String = Struct.new do
17
21
  def resolve(f)
18
22
  self
19
23
  end
24
+
25
+ def rbs_type
26
+ '::String'
27
+ end
20
28
  end
21
29
  Binary = Struct.new(:struct, :length, :display_hint) do
22
30
  def resolve(f)
23
31
  self.struct = struct.resolve(f) if self.struct
24
32
  self
25
33
  end
34
+
35
+ def rbs_type
36
+ 'untyped'
37
+ end
26
38
  end
27
39
  NestedAttributes = Struct.new(:attribute_set) do
40
+ using Generator::Refinements # FIXME:
41
+
28
42
  def resolve(f)
29
43
  self.attribute_set = attribute_set.resolve(f)
30
44
  self
31
45
  end
46
+
47
+ def rbs_type
48
+ 'AttributeSets::' + attribute_set.name.as_class_name
49
+ end
32
50
  end
33
51
  SubMessage = Struct.new(:sub_message, :selector) do
34
52
  def resolve(f)
35
53
  self.sub_message = sub_message.resolve(f)
36
54
  self
37
55
  end
56
+
57
+ def rbs_type
58
+ 'untyped'
59
+ end
38
60
  end
39
61
 
40
62
  Pad = Data.define(:length) do
41
63
  def resolve(f)
42
64
  self
43
65
  end
66
+
67
+ def rbs_type
68
+ 'nil'
69
+ end
44
70
  end
45
- end
46
71
 
72
+ Flag = Data.define do
73
+ def resolve(f)
74
+ self
75
+ end
76
+
77
+ def rbs_type
78
+ '::Integer'
79
+ end
80
+ end
81
+
82
+ Bitfield32 = Data.define do
83
+ def resolve(f)
84
+ self
85
+ end
86
+
87
+ def rbs_type
88
+ '::Integer'
89
+ end
90
+ end
91
+
92
+ IndexedArray = Struct.new(:sub_type) do
93
+ def resolve(f)
94
+ self.sub_type = sub_type.resolve(f)
95
+ self
96
+ end
97
+
98
+ def rbs_type
99
+ 'untyped'
100
+ end
101
+ end
102
+ end
47
103
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ynl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kasumi Hanazuki
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.1.0
18
+ version: 0.2.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.1.0
25
+ version: 0.2.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: yaml
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -45,12 +45,16 @@ extensions: []
45
45
  extra_rdoc_files: []
46
46
  files:
47
47
  - ".rspec"
48
+ - CHANGELOG.md
49
+ - LICENSE.txt
50
+ - README.md
48
51
  - Rakefile
49
52
  - lib/ynl.rb
50
53
  - lib/ynl/family.rb
51
54
  - lib/ynl/generator.rb
52
55
  - lib/ynl/models.rb
53
56
  - lib/ynl/parser.rb
57
+ - lib/ynl/version.rb
54
58
  - test.rb
55
59
  homepage: https://github.com/hanazuki/nl
56
60
  licenses:
@@ -73,7 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
73
77
  - !ruby/object:Gem::Version
74
78
  version: '0'
75
79
  requirements: []
76
- rubygems_version: 3.6.8
80
+ rubygems_version: 4.0.3
77
81
  specification_version: 4
78
82
  summary: Linux Netlink - core
79
83
  test_files: []