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.
@@ -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