unibuf 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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +16 -0
- data/.rubocop_todo.yml +498 -0
- data/README.adoc +490 -0
- data/Rakefile +12 -0
- data/exe/unibuf +7 -0
- data/lib/unibuf/cli.rb +128 -0
- data/lib/unibuf/commands/convert.rb +121 -0
- data/lib/unibuf/commands/parse.rb +85 -0
- data/lib/unibuf/commands/schema.rb +114 -0
- data/lib/unibuf/commands/validate.rb +76 -0
- data/lib/unibuf/errors.rb +36 -0
- data/lib/unibuf/models/enum_definition.rb +78 -0
- data/lib/unibuf/models/field.rb +159 -0
- data/lib/unibuf/models/field_definition.rb +119 -0
- data/lib/unibuf/models/message.rb +203 -0
- data/lib/unibuf/models/message_definition.rb +102 -0
- data/lib/unibuf/models/schema.rb +67 -0
- data/lib/unibuf/models/values/base_value.rb +78 -0
- data/lib/unibuf/models/values/list_value.rb +114 -0
- data/lib/unibuf/models/values/map_value.rb +103 -0
- data/lib/unibuf/models/values/message_value.rb +70 -0
- data/lib/unibuf/models/values/scalar_value.rb +113 -0
- data/lib/unibuf/parsers/binary/wire_format_parser.rb +43 -0
- data/lib/unibuf/parsers/proto3/grammar.rb +149 -0
- data/lib/unibuf/parsers/proto3/processor.rb +188 -0
- data/lib/unibuf/parsers/textproto/grammar.rb +141 -0
- data/lib/unibuf/parsers/textproto/parser.rb +92 -0
- data/lib/unibuf/parsers/textproto/processor.rb +136 -0
- data/lib/unibuf/validators/schema_validator.rb +110 -0
- data/lib/unibuf/validators/type_validator.rb +122 -0
- data/lib/unibuf/version.rb +5 -0
- data/lib/unibuf.rb +207 -0
- data/sig/unibuf.rbs +4 -0
- metadata +139 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Unibuf
|
|
4
|
+
module Models
|
|
5
|
+
# Represents a field definition from a Proto3 schema
|
|
6
|
+
# Used for type checking and validation
|
|
7
|
+
class FieldDefinition
|
|
8
|
+
attr_reader :name, :type, :number, :label, :options
|
|
9
|
+
|
|
10
|
+
# Map types are stored as special attributes
|
|
11
|
+
attr_reader :key_type, :value_type
|
|
12
|
+
|
|
13
|
+
def initialize(attributes = {})
|
|
14
|
+
@name = attributes[:name] || attributes["name"]
|
|
15
|
+
@type = attributes[:type] || attributes["type"]
|
|
16
|
+
@number = attributes[:number] || attributes["number"]
|
|
17
|
+
@label = attributes[:label] || attributes["label"]
|
|
18
|
+
@options = attributes[:options] || attributes["options"] || {}
|
|
19
|
+
|
|
20
|
+
# For map fields
|
|
21
|
+
@key_type = attributes[:key_type] || attributes["key_type"]
|
|
22
|
+
@value_type = attributes[:value_type] || attributes["value_type"]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Type queries - MECE
|
|
26
|
+
def repeated?
|
|
27
|
+
label == "repeated"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def optional?
|
|
31
|
+
label == "optional" || label.nil?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def required?
|
|
35
|
+
label == "required"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def map?
|
|
39
|
+
!key_type.nil? && !value_type.nil?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def message_type?
|
|
43
|
+
!scalar_type? && !map?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def scalar_type?
|
|
47
|
+
SCALAR_TYPES.include?(type)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Scalar types from Protocol Buffers
|
|
51
|
+
SCALAR_TYPES = %w[
|
|
52
|
+
double float int32 int64 uint32 uint64
|
|
53
|
+
sint32 sint64 fixed32 fixed64 sfixed32 sfixed64
|
|
54
|
+
bool string bytes
|
|
55
|
+
].freeze
|
|
56
|
+
|
|
57
|
+
# Validation
|
|
58
|
+
def valid?
|
|
59
|
+
validate!
|
|
60
|
+
true
|
|
61
|
+
rescue ValidationError
|
|
62
|
+
false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def validate!
|
|
66
|
+
raise ValidationError, "Field name required" unless name
|
|
67
|
+
raise ValidationError, "Field type required" unless type
|
|
68
|
+
raise ValidationError, "Field number required" unless number
|
|
69
|
+
|
|
70
|
+
unless number.positive?
|
|
71
|
+
raise ValidationError,
|
|
72
|
+
"Field number must be positive"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def valid_value?(value)
|
|
79
|
+
return true if value.nil? && optional?
|
|
80
|
+
|
|
81
|
+
case type
|
|
82
|
+
when "string"
|
|
83
|
+
value.is_a?(String)
|
|
84
|
+
when "int32", "sint32", "sfixed32"
|
|
85
|
+
value.is_a?(Integer) && value >= -2**31 && value < 2**31
|
|
86
|
+
when "int64", "sint64", "sfixed64"
|
|
87
|
+
value.is_a?(Integer) && value >= -2**63 && value < 2**63
|
|
88
|
+
when "uint32", "fixed32"
|
|
89
|
+
value.is_a?(Integer) && value >= 0 && value < 2**32
|
|
90
|
+
when "uint64", "fixed64"
|
|
91
|
+
value.is_a?(Integer) && value >= 0 && value < 2**64
|
|
92
|
+
when "float", "double"
|
|
93
|
+
value.is_a?(Numeric)
|
|
94
|
+
when "bool"
|
|
95
|
+
value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
96
|
+
when "bytes"
|
|
97
|
+
value.is_a?(String)
|
|
98
|
+
else
|
|
99
|
+
# Message type - allow hash or Message object
|
|
100
|
+
value.is_a?(Hash) || value.is_a?(Message)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Transformation
|
|
105
|
+
def to_h
|
|
106
|
+
hash = {
|
|
107
|
+
name: name,
|
|
108
|
+
type: type,
|
|
109
|
+
number: number,
|
|
110
|
+
}
|
|
111
|
+
hash[:label] = label if label
|
|
112
|
+
hash[:key_type] = key_type if key_type
|
|
113
|
+
hash[:value_type] = value_type if value_type
|
|
114
|
+
hash[:options] = options unless options.empty?
|
|
115
|
+
hash
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "field"
|
|
4
|
+
|
|
5
|
+
module Unibuf
|
|
6
|
+
module Models
|
|
7
|
+
# Represents a Protocol Buffer message
|
|
8
|
+
# Rich domain model with comprehensive behavior
|
|
9
|
+
class Message
|
|
10
|
+
attr_reader :fields
|
|
11
|
+
|
|
12
|
+
def initialize(attributes = {})
|
|
13
|
+
fields_data = attributes["fields"] || attributes[:fields] || []
|
|
14
|
+
@fields = Array(fields_data).map do |field_data|
|
|
15
|
+
field_data.is_a?(Field) ? field_data : Field.new(field_data)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Factory method from hash
|
|
20
|
+
def self.from_hash(hash)
|
|
21
|
+
new(hash)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Classification - MECE principle
|
|
25
|
+
def nested?
|
|
26
|
+
fields_array.any?(&:message_field?)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def repeated_fields?
|
|
30
|
+
# Check if any field name appears multiple times
|
|
31
|
+
field_names = fields_array.map(&:name)
|
|
32
|
+
field_names.uniq.size != field_names.size
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def maps?
|
|
36
|
+
fields_array.any?(&:map_field?)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def scalar_only?
|
|
40
|
+
fields_array.all?(&:scalar_field?)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def empty?
|
|
44
|
+
fields_array.empty?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def complete?
|
|
48
|
+
# Message is complete if it has at least one field
|
|
49
|
+
!empty?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Query methods
|
|
53
|
+
def find_field(name)
|
|
54
|
+
fields_array.find { |f| f.name == name }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def find_fields(name)
|
|
58
|
+
fields_array.select { |f| f.name == name }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def field_names
|
|
62
|
+
fields_array.map(&:name).uniq
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def field_count
|
|
66
|
+
fields_array.size
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def repeated_field_names
|
|
70
|
+
field_names.select { |name| find_fields(name).size > 1 }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def map_fields
|
|
74
|
+
fields_array.select(&:map_field?)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def nested_messages
|
|
78
|
+
fields_array.select(&:message_field?).map(&:as_message)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Traversal methods (Milestone 3)
|
|
82
|
+
def traverse_depth_first(&block)
|
|
83
|
+
return enum_for(:traverse_depth_first) unless block
|
|
84
|
+
|
|
85
|
+
fields_array.each do |field|
|
|
86
|
+
yield field
|
|
87
|
+
if field.message_field?
|
|
88
|
+
field.as_message.traverse_depth_first(&block)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def traverse_breadth_first
|
|
94
|
+
return enum_for(:traverse_breadth_first) unless block_given?
|
|
95
|
+
|
|
96
|
+
queue = fields_array.dup
|
|
97
|
+
|
|
98
|
+
until queue.empty?
|
|
99
|
+
field = queue.shift
|
|
100
|
+
yield field
|
|
101
|
+
|
|
102
|
+
if field.message_field?
|
|
103
|
+
queue.concat(field.as_message.fields_array)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def all_fields_recursive
|
|
109
|
+
traverse_depth_first.to_a
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def depth
|
|
113
|
+
return 0 if empty?
|
|
114
|
+
return 0 unless nested?
|
|
115
|
+
|
|
116
|
+
nested_messages.map { |msg| msg.depth + 1 }.max
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Validation methods (Milestone 3)
|
|
120
|
+
def valid?
|
|
121
|
+
validate!
|
|
122
|
+
true
|
|
123
|
+
rescue ValidationError
|
|
124
|
+
false
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def validate!
|
|
128
|
+
validation_errors.each do |error|
|
|
129
|
+
raise ValidationError, error
|
|
130
|
+
end
|
|
131
|
+
true
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def validation_errors
|
|
135
|
+
errors = []
|
|
136
|
+
|
|
137
|
+
fields_array.each do |field|
|
|
138
|
+
# Check for nil values
|
|
139
|
+
errors << "Field '#{field.name}' has nil value" if field.value.nil?
|
|
140
|
+
|
|
141
|
+
# Validate nested messages
|
|
142
|
+
if field.message_field?
|
|
143
|
+
begin
|
|
144
|
+
field.as_message.validate!
|
|
145
|
+
rescue ValidationError => e
|
|
146
|
+
errors << "Nested message '#{field.name}': #{e.message}"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
errors
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Transformation methods
|
|
155
|
+
def to_h
|
|
156
|
+
{
|
|
157
|
+
"fields" => fields_array.map do |field|
|
|
158
|
+
{
|
|
159
|
+
"name" => field.name,
|
|
160
|
+
"value" => field.value,
|
|
161
|
+
}
|
|
162
|
+
end,
|
|
163
|
+
}
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def to_json(*args)
|
|
167
|
+
require "json"
|
|
168
|
+
to_h.to_json(*args)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def to_yaml
|
|
172
|
+
require "yaml"
|
|
173
|
+
to_h.to_yaml
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Serialize to textproto format
|
|
177
|
+
def to_textproto(indent: 0)
|
|
178
|
+
lines = fields_array.map do |field|
|
|
179
|
+
field.to_textproto(indent: indent)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
lines.join("\n")
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Comparison
|
|
186
|
+
def ==(other)
|
|
187
|
+
return false unless other.is_a?(Message)
|
|
188
|
+
|
|
189
|
+
fields_array == other.send(:fields_array)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def hash
|
|
193
|
+
fields_array.hash
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
protected
|
|
197
|
+
|
|
198
|
+
def fields_array
|
|
199
|
+
@fields || []
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Unibuf
|
|
4
|
+
module Models
|
|
5
|
+
# Represents a message type definition from a .proto schema
|
|
6
|
+
# Used for schema-based validation of textproto files
|
|
7
|
+
class MessageDefinition
|
|
8
|
+
attr_reader :name, :fields, :nested_messages, :nested_enums
|
|
9
|
+
|
|
10
|
+
def initialize(attributes = {})
|
|
11
|
+
@name = attributes[:name] || attributes["name"]
|
|
12
|
+
@fields = Array(attributes[:fields] || attributes["fields"])
|
|
13
|
+
@nested_messages = Array(attributes[:nested_messages] || attributes["nested_messages"])
|
|
14
|
+
@nested_enums = Array(attributes[:nested_enums] || attributes["nested_enums"])
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Queries
|
|
18
|
+
def find_field(name)
|
|
19
|
+
fields.find { |f| f.name == name }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def find_field_by_number(number)
|
|
23
|
+
fields.find { |f| f.number == number }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def find_nested_message(name)
|
|
27
|
+
nested_messages.find { |m| m.name == name }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def find_nested_enum(name)
|
|
31
|
+
nested_enums.find { |e| e.name == name }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def field_names
|
|
35
|
+
fields.map(&:name)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def field_numbers
|
|
39
|
+
fields.map(&:number)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Classification
|
|
43
|
+
def has_repeated_fields?
|
|
44
|
+
fields.any?(&:repeated?)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def has_nested_messages?
|
|
48
|
+
nested_messages.any?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def has_maps?
|
|
52
|
+
fields.any?(&:map?)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Validation
|
|
56
|
+
def valid?
|
|
57
|
+
validate!
|
|
58
|
+
true
|
|
59
|
+
rescue ValidationError
|
|
60
|
+
false
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def validate!
|
|
64
|
+
raise ValidationError, "Message name required" unless name
|
|
65
|
+
|
|
66
|
+
# Check for duplicate field numbers
|
|
67
|
+
numbers = field_numbers
|
|
68
|
+
duplicates = numbers.select { |n| numbers.count(n) > 1 }.uniq
|
|
69
|
+
if duplicates.any?
|
|
70
|
+
raise ValidationError,
|
|
71
|
+
"Duplicate field numbers: #{duplicates.join(', ')}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Validate all fields
|
|
75
|
+
fields.each(&:validate!)
|
|
76
|
+
|
|
77
|
+
# Validate nested messages
|
|
78
|
+
nested_messages.each(&:validate!)
|
|
79
|
+
nested_enums.each(&:validate!)
|
|
80
|
+
|
|
81
|
+
true
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def valid_field_value?(field_name, value)
|
|
85
|
+
field_def = find_field(field_name)
|
|
86
|
+
return false unless field_def
|
|
87
|
+
|
|
88
|
+
field_def.valid_value?(value)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Transformation
|
|
92
|
+
def to_h
|
|
93
|
+
{
|
|
94
|
+
name: name,
|
|
95
|
+
fields: fields.map(&:to_h),
|
|
96
|
+
nested_messages: nested_messages.map(&:to_h),
|
|
97
|
+
nested_enums: nested_enums.map(&:to_h),
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Unibuf
|
|
4
|
+
module Models
|
|
5
|
+
# Represents a Protocol Buffer schema (.proto file)
|
|
6
|
+
class Schema
|
|
7
|
+
attr_reader :syntax, :package, :imports, :messages, :enums
|
|
8
|
+
|
|
9
|
+
def initialize(attributes = {})
|
|
10
|
+
@syntax = attributes[:syntax] || attributes["syntax"] || "proto3"
|
|
11
|
+
@package = attributes[:package] || attributes["package"]
|
|
12
|
+
@imports = Array(attributes[:imports] || attributes["imports"])
|
|
13
|
+
@messages = Array(attributes[:messages] || attributes["messages"])
|
|
14
|
+
@enums = Array(attributes[:enums] || attributes["enums"])
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Queries
|
|
18
|
+
def find_message(name)
|
|
19
|
+
messages.find { |msg| msg.name == name }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def find_enum(name)
|
|
23
|
+
enums.find { |enum| enum.name == name }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def message_names
|
|
27
|
+
messages.map(&:name)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def enum_names
|
|
31
|
+
enums.map(&:name)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Validation
|
|
35
|
+
def valid?
|
|
36
|
+
validate!
|
|
37
|
+
true
|
|
38
|
+
rescue ValidationError
|
|
39
|
+
false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def validate!
|
|
43
|
+
raise ValidationError, "Syntax must be proto3" unless syntax == "proto3"
|
|
44
|
+
|
|
45
|
+
messages.each(&:validate!)
|
|
46
|
+
enums.each(&:validate!)
|
|
47
|
+
|
|
48
|
+
true
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Find type (message or enum)
|
|
52
|
+
def find_type(name)
|
|
53
|
+
find_message(name) || find_enum(name)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def to_h
|
|
57
|
+
{
|
|
58
|
+
syntax: syntax,
|
|
59
|
+
package: package,
|
|
60
|
+
imports: imports,
|
|
61
|
+
messages: messages.map(&:to_h),
|
|
62
|
+
enums: enums.map(&:to_h),
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Unibuf
|
|
4
|
+
module Models
|
|
5
|
+
module Values
|
|
6
|
+
# Base class for all value types in Protocol Buffer messages
|
|
7
|
+
# Follows Open/Closed principle - open for extension, closed for modification
|
|
8
|
+
class BaseValue
|
|
9
|
+
attr_reader :raw_value
|
|
10
|
+
|
|
11
|
+
def initialize(raw_value)
|
|
12
|
+
@raw_value = raw_value
|
|
13
|
+
validate!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Type identification - MECE classification
|
|
17
|
+
def scalar?
|
|
18
|
+
false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def message?
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def list?
|
|
26
|
+
false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def map?
|
|
30
|
+
false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Serialization - template method pattern
|
|
34
|
+
def to_textproto(indent: 0)
|
|
35
|
+
raise NotImplementedError, "Subclasses must implement to_textproto"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Validation - template method pattern
|
|
39
|
+
def validate!
|
|
40
|
+
# Subclasses can override for specific validation
|
|
41
|
+
true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Equality
|
|
45
|
+
def ==(other)
|
|
46
|
+
return false unless other.is_a?(self.class)
|
|
47
|
+
|
|
48
|
+
raw_value == other.raw_value
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def hash
|
|
52
|
+
[self.class, raw_value].hash
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Factory method - creates appropriate value type
|
|
56
|
+
def self.from_raw(raw_value)
|
|
57
|
+
case raw_value
|
|
58
|
+
when Hash
|
|
59
|
+
if raw_value.key?("fields")
|
|
60
|
+
MessageValue.new(raw_value)
|
|
61
|
+
elsif raw_value.key?("key") && raw_value.key?("value")
|
|
62
|
+
MapValue.new(raw_value)
|
|
63
|
+
else
|
|
64
|
+
raise InvalidValueError,
|
|
65
|
+
"Unknown hash structure: #{raw_value.keys}"
|
|
66
|
+
end
|
|
67
|
+
when Array
|
|
68
|
+
ListValue.new(raw_value)
|
|
69
|
+
when String, Integer, Float, TrueClass, FalseClass, NilClass
|
|
70
|
+
ScalarValue.new(raw_value)
|
|
71
|
+
else
|
|
72
|
+
raise InvalidValueError, "Unknown value type: #{raw_value.class}"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_value"
|
|
4
|
+
|
|
5
|
+
module Unibuf
|
|
6
|
+
module Models
|
|
7
|
+
module Values
|
|
8
|
+
# Represents a list/array value (repeated fields)
|
|
9
|
+
# Provides array-like interface with type safety
|
|
10
|
+
class ListValue < BaseValue
|
|
11
|
+
def list?
|
|
12
|
+
true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Array-like interface
|
|
16
|
+
def size
|
|
17
|
+
items.size
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def empty?
|
|
21
|
+
items.empty?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def [](index)
|
|
25
|
+
items[index]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def each(&)
|
|
29
|
+
items.each(&)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def map(&)
|
|
33
|
+
items.map(&)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def select(&)
|
|
37
|
+
items.select(&)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def first
|
|
41
|
+
items.first
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def last
|
|
45
|
+
items.last
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Type checking
|
|
49
|
+
def homogeneous?
|
|
50
|
+
return true if items.empty?
|
|
51
|
+
|
|
52
|
+
first_type = items.first.class
|
|
53
|
+
items.all?(first_type)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def all_scalars?
|
|
57
|
+
items.all? do |item|
|
|
58
|
+
item.is_a?(String) || item.is_a?(Numeric) || item.is_a?(TrueClass) || item.is_a?(FalseClass)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def all_messages?
|
|
63
|
+
items.all? { |item| item.is_a?(Hash) && item.key?("fields") }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Serialization
|
|
67
|
+
def to_textproto(indent: 0)
|
|
68
|
+
return "[]" if empty?
|
|
69
|
+
|
|
70
|
+
if all_scalars? && size < 5
|
|
71
|
+
# Short inline format for small scalar lists
|
|
72
|
+
formatted = items.map { |item| format_item(item) }
|
|
73
|
+
"[#{formatted.join(', ')}]"
|
|
74
|
+
else
|
|
75
|
+
# Multi-line format for complex or large lists
|
|
76
|
+
indent_str = " " * indent
|
|
77
|
+
formatted = items.map { |item| format_item(item) }
|
|
78
|
+
"[\n#{indent_str} #{formatted.join(",\n#{indent_str} ")}\n#{indent_str}]"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Validation
|
|
83
|
+
def validate!
|
|
84
|
+
unless raw_value.is_a?(Array)
|
|
85
|
+
raise InvalidValueError,
|
|
86
|
+
"ListValue requires array, got #{raw_value.class}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
true
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def items
|
|
95
|
+
@items ||= Array(raw_value)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def format_item(item)
|
|
99
|
+
case item
|
|
100
|
+
when String
|
|
101
|
+
"\"#{item.gsub('\\', '\\\\').gsub('"', '\"')}\""
|
|
102
|
+
when Numeric, TrueClass, FalseClass
|
|
103
|
+
item.to_s
|
|
104
|
+
when Hash
|
|
105
|
+
msg = Models::Message.new(item)
|
|
106
|
+
msg.to_textproto
|
|
107
|
+
else
|
|
108
|
+
item.to_s
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|