ynl 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b6a0856ea9a2918cc0a1dee7d9452227091209776df336059a4eae4ca85b903e
4
+ data.tar.gz: 4513210c607ad814734cc0ed03f5e57ed1853da3b86539015a933db6364c5205
5
+ SHA512:
6
+ metadata.gz: ba1fd6ff654ad9c613e98ddcbdf9e28e3b9b24eac873f5e1c19aa2cfa919e2020bc9c7557691d235554f72cc279f89e4612027aef1feaeda43c3d9a98201f76d
7
+ data.tar.gz: 39ca59b427aa8d8c945350a351518063407583e7e511acf0e4ed5ba901b860dbab65b32daaf0a1218ccdbf7fddd6d0a5c826761f80393f88b361159a8fee4be9
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
data/lib/ynl/family.rb ADDED
@@ -0,0 +1,20 @@
1
+ require 'stringio'
2
+
3
+ require_relative 'parser'
4
+ require_relative 'generator'
5
+
6
+ module Ynl
7
+ class Family
8
+ def self.build(path)
9
+ require 'nl'
10
+
11
+ defs = Ynl::Parser.parse_file(path)
12
+
13
+ buf = StringIO.new
14
+ classname = Generator.new(defs, buf).generate(namespace: 'self')
15
+ classdef = buf.string
16
+
17
+ Module.new { eval(classdef) }.const_get(classname)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,230 @@
1
+ module Ynl
2
+ class Generator
3
+ module Refinements
4
+ WORD_DELIM = /[ _-]+/
5
+
6
+ refine(String) do
7
+ def as_const_name
8
+ split(WORD_DELIM).map(&:upcase).join('_')
9
+ end
10
+
11
+ def as_class_name
12
+ split(WORD_DELIM).map(&:capitalize).join
13
+ end
14
+
15
+ def as_variable_name
16
+ split(WORD_DELIM).join('_')
17
+ end
18
+ alias as_method_name as_variable_name
19
+
20
+ def as_string_literal
21
+ dump
22
+ end
23
+
24
+ def as_symbol_literal
25
+ ":#{dump}"
26
+ end
27
+ end
28
+ end
29
+ private_constant :Refinements
30
+
31
+ using Refinements
32
+
33
+ def initialize(ynl, out)
34
+ @ynl = ynl
35
+ @out = out
36
+ @indent = 0
37
+ end
38
+
39
+ def generate(superclass: '::Nl::Family', namespace: nil)
40
+ classname = @ynl.name.as_class_name
41
+ emit_class([*namespace, classname].join('::'), superclass) do
42
+ write('NAME = ', @ynl.name.as_string_literal)
43
+ write('PROTONUM = ', @ynl.protonum)
44
+
45
+ if @ynl.protocol == 'netlink-raw'
46
+ write('PROTOCOL = Ractor.make_shareable(::Nl::Protocols::Raw.new(', @ynl.name.as_string_literal, ', ', @ynl.protonum ,'))')
47
+ else
48
+ write('PROTOCOL = Ractor.make_shareable(::Nl::Protocols::Genl.new(', @ynl.name.as_string_literal, ')')
49
+ end
50
+
51
+ emit_module('Structs') do
52
+ @ynl.structs.each do |name, struct|
53
+ emit_comment(struct.doc)
54
+ write(name.as_class_name, ' = Struct.new(', struct.members.map { it.name.as_variable_name.as_symbol_literal }.join(', ') ,')')
55
+ 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(', ') ,'})')
57
+ write('def self.decode(decoder)')
58
+ indent do
59
+ write('self.new(*MEMBERS.map {|name, datatype| datatype.decode(decoder) })')
60
+ end
61
+ write('end')
62
+
63
+ write('def encode(encoder)')
64
+ indent do
65
+ write('MEMBERS.each {|name, datatype| datatype.encode(encoder, self.public_send(name)) }')
66
+ end
67
+ write('end')
68
+ end
69
+ end
70
+ end
71
+
72
+ emit_module('AttributeSets') do
73
+ @ynl.attribute_sets.each do |name, attr_set|
74
+ emit_comment(attr_set.doc)
75
+ emit_class(name.as_class_name, '::Nl::Family::AttributeSet') do
76
+ emit_comment("Abstract class")
77
+ emit_class('Attribute', '::Nl::Family::AttributeSet::Attribute') do
78
+ end
79
+ attr_set.attributes.each do |attr|
80
+ emit_comment(attr.doc)
81
+ 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))
85
+ end
86
+ end
87
+
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(', ') ,'})')
90
+
91
+ emit_singleton_class do
92
+ emit_rbs_comment(
93
+ 'name: Symbol',
94
+ 'return: Attribute',
95
+ )
96
+ write('def by_name(name) = BY_NAME[name]')
97
+
98
+ emit_rbs_comment(
99
+ 'type: Integer',
100
+ 'return: Attribute',
101
+ )
102
+ write('def by_type(type) = BY_TYPE[type]')
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ emit_module('Messages') do
109
+ @ynl.operations.each do |name, oper|
110
+ emit_comment(oper.doc)
111
+ %w[do dump].each do |method|
112
+ if request_reply = oper.public_send(method + 'it')
113
+ %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)
118
+ 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(' ') ,'])')
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ # emit request methods
131
+ @ynl.operations.each do |name, oper|
132
+ %w[do dump].each do |method|
133
+ if request_reply = oper.public_send(method + 'it')
134
+ 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'
142
+ end
143
+ write("exchange_message(#{method.as_symbol_literal}, #{request_class}, #{reply_class}, args)")
144
+ end
145
+ write('end')
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ classname
152
+ end
153
+
154
+ INDENT = -' '
155
+ NEWLINE = -?\n
156
+ private_constant :INDENT, :NEWLINE
157
+
158
+ private def write(*str)
159
+ @out.write(INDENT * @indent, *str, NEWLINE)
160
+ end
161
+
162
+ private def indent
163
+ @indent += 1
164
+ yield
165
+ ensure
166
+ @indent -= 1
167
+ end
168
+
169
+ private def emit_module(name)
170
+ write('module ', name)
171
+ indent { yield }
172
+ write('end')
173
+ end
174
+
175
+ private def emit_class(name, superclass = nil)
176
+ if superclass
177
+ write('class ', name, ' < ', superclass)
178
+ else
179
+ write('class ', name)
180
+ end
181
+ indent { yield }
182
+ write('end')
183
+ end
184
+
185
+ private def emit_singleton_class
186
+ write('class << self')
187
+ indent { yield }
188
+ write('end')
189
+ end
190
+
191
+ private def emit_comment(comment)
192
+ return unless comment
193
+ comment.each_line do |line|
194
+ write('# ', line)
195
+ end
196
+ end
197
+
198
+ private def emit_rbs_comment(*args)
199
+ args.each do |arg|
200
+ write('# @rbs ', arg)
201
+ end
202
+ end
203
+
204
+ private def to_datatype(type, checks)
205
+ case type
206
+ when Types::Pad
207
+ 'nil'
208
+ 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)})"
210
+ when Types::String
211
+ "PROTOCOL.class::DataTypes::String.new(check: #{to_checks(checks)})"
212
+ when Types::Binary
213
+ # if type.struct
214
+ # "Structs::" + type.struct.name.as_class_name
215
+ # else
216
+ "PROTOCOL.class::DataTypes::Binary.new(check: #{to_checks(checks)})"
217
+ # end
218
+ when Types::NestedAttributes
219
+ "PROTOCOL.class::DataTypes::NestedAttributes.new(#{type.attribute_set.name.as_class_name})"
220
+ else
221
+ raise "Unknown type: #{type.class}"
222
+ end
223
+ end
224
+
225
+ private def to_checks(checks)
226
+ return 'nil' if !checks || checks.empty?
227
+ %Q{-> { #{checks.join('; ')} }}
228
+ end
229
+ end
230
+ end
data/lib/ynl/models.rb ADDED
@@ -0,0 +1,156 @@
1
+ module Ynl
2
+ module Models
3
+ Family = ::Struct.new(:name, :protocol, :protonum, :doc,
4
+ :consts, :enums, :flags, :structs,
5
+ :attribute_sets, :sub_messages, :operations, :mcast_groups)
6
+ class Family
7
+ def resolve
8
+ structs.transform_values! {|s| s.resolve(self) }
9
+ attribute_sets.transform_values! {|s| s.resolve(self) }
10
+ self
11
+ end
12
+
13
+ def find_type(name)
14
+ enums[name] or flags[name] or structs[name] or raise ParseError, "Unknown type: #{name}"
15
+ end
16
+ end
17
+
18
+ class Thunk
19
+ def initialize(&block)
20
+ @block = block
21
+ end
22
+
23
+ def resolve(f)
24
+ raise ParseError, "Circular dependency" if @resolving
25
+ @resolving = true
26
+ @block.call(f)
27
+ end
28
+ end
29
+
30
+ class Enum
31
+ Entry = ::Struct.new(:name, :value, :doc)
32
+
33
+ attr_reader :name, :entries, :doc
34
+
35
+ def initialize(name:, doc:)
36
+ @name = name
37
+ @entries = []
38
+ @doc = doc
39
+ end
40
+
41
+ def resolve(f)
42
+ self
43
+ end
44
+ end
45
+
46
+ class Flags
47
+ Entry = ::Struct.new(:name, :value, :doc)
48
+
49
+ attr_reader :name, :entries, :doc
50
+
51
+ def initialize(name:, doc:)
52
+ @name = name
53
+ @entries = []
54
+ @doc = doc
55
+ end
56
+
57
+ def resolve(f)
58
+ self
59
+ end
60
+ end
61
+
62
+ class Struct
63
+ Member = ::Struct.new(:name, :type, :doc) do
64
+ def resolve(f)
65
+ self.type = self.type.resolve(f)
66
+ self
67
+ end
68
+ end
69
+
70
+ attr_reader :name, :members, :doc
71
+
72
+ def initialize(name:, doc:)
73
+ @name = name
74
+ @members = []
75
+ @doc = doc
76
+ end
77
+
78
+ def resolve(f)
79
+ @members.map! {|m| m.resolve(f) }
80
+ self
81
+ end
82
+ end
83
+
84
+ class AttributeSet
85
+ Attribute = ::Struct.new(:name, :type, :value, :checks, :doc) do
86
+ def resolve(f)
87
+ self.type = type.resolve(f)
88
+ self
89
+ end
90
+ end
91
+
92
+ attr_reader :name, :name_prefix, :attributes, :doc
93
+
94
+ def initialize(name:, name_prefix:, doc:)
95
+ @name = name
96
+ @name_prefix = name_prefix
97
+ @attributes = []
98
+ @doc = doc
99
+ end
100
+
101
+ def resolve(f)
102
+ attributes.map! { it.resolve(f) }
103
+ self
104
+ rescue
105
+ raise ParseError, "Failed to resolve attribute set: #{name}"
106
+ end
107
+ end
108
+
109
+ class Operation
110
+ attr_reader :name, :doc, :fixed_header, :attribute_set, :doit, :dumpit
111
+
112
+ def initialize(name:, doc:, fixed_header:, attribute_set:, doit:, dumpit:)
113
+ @name = name
114
+ @doc = doc
115
+ @fixed_header = fixed_header
116
+ @attribute_set = attribute_set
117
+ @doit = doit
118
+ @dumpit = dumpit
119
+ end
120
+
121
+ def resolve(f)
122
+ @doit = @doit.resolve(f)
123
+ @dumpit = @dumpit.resolve(f)
124
+ self
125
+ end
126
+ end
127
+
128
+ class RequestReply
129
+ attr_reader :request, :reply
130
+
131
+ def initialize(request:, reply:)
132
+ @request = request
133
+ @reply = reply
134
+ end
135
+
136
+ def resolve(f)
137
+ @request = @request.resolve(f)
138
+ @reply = @reply.resolve(f)
139
+ self
140
+ end
141
+ end
142
+
143
+ class Message
144
+ attr_reader :value, :attributes
145
+
146
+ def initialize(value:, attributes:)
147
+ @value = value
148
+ @attributes = attributes
149
+ end
150
+
151
+ def resolve(f)
152
+ self
153
+ end
154
+ end
155
+ end
156
+ end
data/lib/ynl/parser.rb ADDED
@@ -0,0 +1,330 @@
1
+ require 'yaml'
2
+
3
+ require_relative 'models'
4
+
5
+ module Ynl
6
+ class Parser
7
+ def initialize(source)
8
+ @yaml = YAML.load(source, aliases: true)
9
+
10
+ @consts = {}
11
+ @flags = {}
12
+ @enums = {}
13
+ @structs = {}
14
+
15
+ @attribute_sets = {}
16
+ @sub_messages = {}
17
+ @operations = {}
18
+ @mcast_groups = {}
19
+ end
20
+
21
+ def self.parse_file(path)
22
+ File.open(path) {|f| new(f) }.parse
23
+ end
24
+
25
+ def parse
26
+ protocol = @yaml['protocol'] || 'genetlink'
27
+ protonum = @yaml['protonum']
28
+ name = @yaml['name']
29
+ doc = @yaml['doc']
30
+
31
+ @yaml['definitions']&.each do |d|
32
+ parse_definition(d)
33
+ end
34
+ @yaml['attribute-sets'].each do |d|
35
+ parse_attribute_set(d)
36
+ end
37
+ @yaml['sub-messages']&.each do |d|
38
+ parse_sub_message(d)
39
+ end
40
+ if operations = @yaml['operations']
41
+ enum_model = case operations['enum-model']
42
+ when 'directional'
43
+ :directional
44
+ when nil
45
+ :unidirectional
46
+ else
47
+ raise ParseError, "Unknown enum model: #{operations['enum-model']}"
48
+ end
49
+ @default_fixed_header = operations['fixed-header']&.then { @structs.fetch(it) }
50
+ operations['list']&.each do |d|
51
+ parse_operation(d, enum_model:)
52
+ end
53
+ end
54
+ @yaml['mcast-groups']&.each do |d|
55
+ parse_mcast_group(d)
56
+ end
57
+
58
+ Models::Family.new(
59
+ name:,
60
+ protocol:,
61
+ protonum:,
62
+ doc:,
63
+ consts: @consts,
64
+ enums: @enums,
65
+ flags: @flags,
66
+ structs: @structs,
67
+ attribute_sets: @attribute_sets,
68
+ sub_messages: @sub_messages,
69
+ operations: @operations,
70
+ mcast_groups: @mcast_groups,
71
+ ).resolve
72
+ end
73
+
74
+ private def parse_definition(d)
75
+ case type = d.fetch('type')
76
+ when 'const'
77
+ v = parse_const(d)
78
+ @consts[v.name] = v
79
+ when 'enum'
80
+ v = parse_enum_flags(d, type: :enum)
81
+ @enums[v.name] = v
82
+ when 'flags'
83
+ v = parse_enum_flags(d, type: :flags)
84
+ @flags[v.name] = v
85
+ when 'struct'
86
+ v = parse_struct(d)
87
+ @structs[v.name] = v
88
+ else
89
+ raise ParseError, "Unknown definition type: #{type}"
90
+ end
91
+ end
92
+
93
+ private def parse_enum_flags(d, type:)
94
+ cls = type == :enum ? Models::Enum : Models::Flags
95
+ result = cls.new(name: d.fetch('name'), doc: d['doc'])
96
+
97
+ start_value = d['start-value'] || 0
98
+ value = type == :enum ? start_value : 1 << start_value
99
+
100
+ d.fetch('entries').each do |v|
101
+ case v
102
+ when String
103
+ entry = cls::Entry.new(name: v, value:)
104
+ when Hash
105
+ entry = cls::Entry.new(name: v.fetch('name'), value:, doc: v['doc'])
106
+ else
107
+ raise ParseError, "Unknown class for enum/flags entry: #{v.class}"
108
+ end
109
+
110
+ result.entries << entry
111
+
112
+ value = type == :enum ? value + 1 : value << 1
113
+
114
+ rescue
115
+ raise ParseError, "Failed to parse enum/flags entry: #{v.fetch('name')}"
116
+ end
117
+
118
+ result
119
+ rescue
120
+ raise ParseError, "Failed to parse enum/flags: #{d.fetch('name')}"
121
+ end
122
+
123
+ private def parse_struct_member_type(d)
124
+ type = d.fetch('type')
125
+ case type
126
+ when 'u8', 'u16', 'u32', 'u64', 's8', 's16', 's32', 's64', 'int', 'uint'
127
+ Types::Scalar.new(
128
+ type: type,
129
+ byte_order: parse_byte_order(d['byte-order']),
130
+ )
131
+ when 'binary'
132
+ Types::Binary.new(
133
+ struct: d['struct'] ? Models::Thunk.new {|f| f.structs.fetch(d['struct']) } : nil,
134
+ display_hint: d['display-hint'],
135
+ )
136
+ when 'pad'
137
+ Types::Pad.new(
138
+ length: d['len'],
139
+ )
140
+ else
141
+ fail "Unknown type: #{type}"
142
+ end
143
+ end
144
+
145
+ private def parse_attribute_type(d)
146
+ type = d.fetch('type')
147
+ case type
148
+ when 'u8', 'u16', 'u32', 'u64', 's8', 's16', 's32', 's64', 'int', 'uint'
149
+ Types::Scalar.new(
150
+ type: type,
151
+ byte_order: parse_byte_order(d['byte-order']),
152
+ )
153
+ when 'binary'
154
+ Types::Binary.new(
155
+ struct: d['struct'] ? Models::Thunk.new {|f| f.structs.fetch(d.fetch('struct')) } : nil,
156
+ display_hint: d['display-hint'],
157
+ )
158
+ when 'string'
159
+ Types::String.new
160
+ when 'nest'
161
+ Types::NestedAttributes.new(
162
+ attribute_set: Models::Thunk.new {|f| f.attribute_sets.fetch(d.fetch('nested-attributes')) },
163
+ )
164
+ when 'sub-message'
165
+ Types::SubMessage.new(
166
+ sub_message: Models::Thunk.new {|f| f.sub_messages.fetch(d.fetch('sub-message')) },
167
+ selector: d.fetch('selector'),
168
+ )
169
+ when 'pad'
170
+ Types::Pad.new(
171
+ length: nil,
172
+ )
173
+ when 'unused'
174
+ nil
175
+ else
176
+ raise ParseError, "Unknown type: #{type}"
177
+ end
178
+ end
179
+
180
+ private def parse_byte_order(v)
181
+ case v
182
+ when nil
183
+ :host
184
+ when 'big-endian'
185
+ :big
186
+ when 'litten-endian'
187
+ :little
188
+ else
189
+ raise ParseError, "Unknown endian: #{v}"
190
+ end
191
+ end
192
+
193
+ private def parse_struct(d)
194
+ result = Models::Struct.new(name: d.fetch('name'), doc: d['doc'])
195
+
196
+ d.fetch('members').each do |v|
197
+ type = parse_struct_member_type(v)
198
+ member = Models::Struct::Member.new(name: v.fetch('name'), type: type, doc: v['doc'])
199
+ result.members << member
200
+ rescue
201
+ raise ParseError, "Failed to parse struct member: #{v.fetch('name')}"
202
+ end
203
+
204
+ result
205
+ rescue
206
+ raise ParseError, "Failed to parse struct: #{d.fetch('name')}"
207
+ end
208
+
209
+ private def parse_checks(d)
210
+ d.map do |op, value|
211
+ parse_check(op, value)
212
+ end
213
+ rescue
214
+ raise ParseError, "Failed to parse checks"
215
+ end
216
+
217
+ private def parse_check(op, value_literal)
218
+ case op
219
+ when 'max'
220
+ value = parse_value(value_literal)
221
+ return %{raise unless it <= #{value}}
222
+ when 'min'
223
+ value = parse_value(value_literal)
224
+ return %{raise unless it >= #{value}}
225
+ when 'min-len'
226
+ value = parse_value(value_literal)
227
+ return %{raise unless it.bytesize >= #{value}}
228
+ when 'max-len'
229
+ value = parse_value(value_literal)
230
+ return %{raise unless it.bytesize <= #{value}}
231
+ else
232
+ raise ParseError, "Unknown check: #{op}"
233
+ end
234
+ rescue
235
+ raise ParseError, "Failed to parse check: #{op}"
236
+ end
237
+
238
+ private def parse_value(v)
239
+ case v
240
+ when Integer
241
+ v
242
+ when 'u32-max'
243
+ (2 ** 32) - 1
244
+ when 's32-max'
245
+ (2 ** 31) - 1
246
+ else
247
+ raise ParseError, "Unknown value: #{v}"
248
+ end
249
+ end
250
+
251
+ private def parse_attribute_set(d)
252
+ name = d.fetch('name')
253
+ 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
267
+ else
268
+ name_prefix = d['name_prefix']
269
+ result = Models::AttributeSet.new(name:, name_prefix:, doc: d['doc'])
270
+ value = 0
271
+
272
+ d.fetch('attributes').each do |v|
273
+ if type = parse_attribute_type(v)
274
+ value = v.fetch('value', value + 1)
275
+ attribute = Models::AttributeSet::Attribute.new(name: v.fetch('name'), type: type, value:)
276
+ result.attributes << attribute
277
+ if checks = v['checks']
278
+ attribute.checks = parse_checks(checks)
279
+ end
280
+ end
281
+ rescue
282
+ raise ParseError, "Failed to parse attribute: #{v.fetch('name')}"
283
+ end
284
+ end
285
+
286
+ @attribute_sets[name] = result
287
+
288
+ rescue
289
+ raise ParseError, "Failed to parse attribute set: #{d.fetch('name')}"
290
+ end
291
+
292
+ private def parse_sub_message(d)
293
+ end
294
+
295
+ private def parse_operation(d, enum_model:)
296
+ name = d.fetch('name')
297
+
298
+ fixed_header = d['fixed-header']&.then do
299
+ @structs.fetch(it)
300
+ rescue
301
+ raise ParseError, "Undefined fixed header: #{it}"
302
+ end || @default_fixed_header
303
+
304
+ attribute_set = d['attribute-set']&.then do
305
+ @attribute_sets.fetch(it)
306
+ rescue
307
+ raise ParseError, "Undefined attribute set: #{it}"
308
+ end
309
+
310
+ doit = d['do']&.then { parse_request_reply(it) }
311
+ dumpit = d['dump']&.then { parse_request_reply(it) }
312
+
313
+ @operations[name] = Models::Operation.new(name:, doc: d['doc'], fixed_header:, attribute_set:, doit:, dumpit:)
314
+ end
315
+
316
+ private def parse_request_reply(d)
317
+ Models::RequestReply.new(
318
+ request: d['request']&.then { parse_message(it) },
319
+ reply: d['reply']&.then { parse_message(it) },
320
+ )
321
+ end
322
+
323
+ private def parse_message(d)
324
+ Models::Message.new(value: d['value'], attributes: d.fetch('attributes', []))
325
+ end
326
+
327
+ private def parse_mcast_group(d)
328
+ end
329
+ end
330
+ end
data/lib/ynl.rb ADDED
@@ -0,0 +1,47 @@
1
+ # Parse for YNL netlink specification
2
+ #
3
+ # See: https://www.kernel.org/doc/html/latest/userspace-api/netlink/specs.html
4
+
5
+ require_relative 'ynl/family'
6
+
7
+ module Ynl
8
+ class ParseError < StandardError; end
9
+
10
+ module Types
11
+ Scalar = Data.define(:type, :byte_order) do
12
+ def resolve(f)
13
+ self
14
+ end
15
+ end
16
+ String = Struct.new do
17
+ def resolve(f)
18
+ self
19
+ end
20
+ end
21
+ Binary = Struct.new(:struct, :length, :display_hint) do
22
+ def resolve(f)
23
+ self.struct = struct.resolve(f) if self.struct
24
+ self
25
+ end
26
+ end
27
+ NestedAttributes = Struct.new(:attribute_set) do
28
+ def resolve(f)
29
+ self.attribute_set = attribute_set.resolve(f)
30
+ self
31
+ end
32
+ end
33
+ SubMessage = Struct.new(:sub_message, :selector) do
34
+ def resolve(f)
35
+ self.sub_message = sub_message.resolve(f)
36
+ self
37
+ end
38
+ end
39
+
40
+ Pad = Data.define(:length) do
41
+ def resolve(f)
42
+ self
43
+ end
44
+ end
45
+ end
46
+
47
+ end
data/test.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'nl'
2
+ require 'ynl'
3
+ yaml = File.join(__dir__, '../nl-linux/linux/rt_route.yaml')
4
+ cls = Ynl::Family.build(yaml)
5
+
6
+ #p result = cls.open.dump_getaddr(ifa_index: 1)
7
+ p result = cls.open.dump_getroute(rtm_family: 1)
8
+ binding.irb
9
+
10
+ #p cls.open.dump_get_stats
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ynl
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kasumi Hanazuki
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: nl
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - '='
17
+ - !ruby/object:Gem::Version
18
+ version: 0.1.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - '='
24
+ - !ruby/object:Gem::Version
25
+ version: 0.1.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: yaml
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ description: Linux Netlink
41
+ email:
42
+ - kasumi@rollingapple.net
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - ".rspec"
48
+ - Rakefile
49
+ - lib/ynl.rb
50
+ - lib/ynl/family.rb
51
+ - lib/ynl/generator.rb
52
+ - lib/ynl/models.rb
53
+ - lib/ynl/parser.rb
54
+ - test.rb
55
+ homepage: https://github.com/hanazuki/nl
56
+ licenses:
57
+ - MIT
58
+ metadata:
59
+ homepage_uri: https://github.com/hanazuki/nl
60
+ source_code_uri: https://github.com/hanazuki/nl
61
+ changelog_uri: https://github.com/hanazuki/nl/blob/master/ynl/CHANGELOG.md
62
+ rdoc_options: []
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '3.1'
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ requirements: []
76
+ rubygems_version: 3.6.8
77
+ specification_version: 4
78
+ summary: Linux Netlink - core
79
+ test_files: []