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,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_value"
|
|
4
|
+
|
|
5
|
+
module Unibuf
|
|
6
|
+
module Models
|
|
7
|
+
module Values
|
|
8
|
+
# Represents a map/dictionary value (key-value pairs)
|
|
9
|
+
# Provides hash-like interface with type safety
|
|
10
|
+
class MapValue < BaseValue
|
|
11
|
+
def map?
|
|
12
|
+
true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Hash-like interface
|
|
16
|
+
def key
|
|
17
|
+
raw_value["key"]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def value
|
|
21
|
+
raw_value["value"]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
{ key => value }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Type checking
|
|
29
|
+
def key_type
|
|
30
|
+
key.class
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def value_type
|
|
34
|
+
value.class
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def scalar_value?
|
|
38
|
+
!value.is_a?(Hash) && !value.is_a?(Array)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def message_value?
|
|
42
|
+
value.is_a?(Hash) && value.key?("fields")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Serialization
|
|
46
|
+
def to_textproto(indent: 0)
|
|
47
|
+
indent_str = " " * indent
|
|
48
|
+
key_str = format_value(key)
|
|
49
|
+
val_str = format_value(value)
|
|
50
|
+
|
|
51
|
+
"{\n" \
|
|
52
|
+
"#{indent_str} key: #{key_str}\n" \
|
|
53
|
+
"#{indent_str} value: #{val_str}\n" \
|
|
54
|
+
"#{indent_str}}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Validation
|
|
58
|
+
def validate!
|
|
59
|
+
unless raw_value.is_a?(Hash)
|
|
60
|
+
raise InvalidValueError,
|
|
61
|
+
"MapValue requires hash, got #{raw_value.class}"
|
|
62
|
+
end
|
|
63
|
+
unless raw_value.key?("key") && raw_value.key?("value")
|
|
64
|
+
raise InvalidValueError, "MapValue requires 'key' and 'value' keys"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
true
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Comparison
|
|
71
|
+
def ==(other)
|
|
72
|
+
return false unless other.is_a?(MapValue)
|
|
73
|
+
|
|
74
|
+
key == other.key && value == other.value
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def hash
|
|
78
|
+
[self.class, key, value].hash
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def format_value(val)
|
|
84
|
+
case val
|
|
85
|
+
when String
|
|
86
|
+
"\"#{val.gsub('\\', '\\\\').gsub('"', '\"')}\""
|
|
87
|
+
when Numeric, TrueClass, FalseClass
|
|
88
|
+
val.to_s
|
|
89
|
+
when Hash
|
|
90
|
+
if val.key?("fields")
|
|
91
|
+
msg = Models::Message.new(val)
|
|
92
|
+
"{\n#{msg.to_textproto(indent: 1)}\n}"
|
|
93
|
+
else
|
|
94
|
+
val.to_s
|
|
95
|
+
end
|
|
96
|
+
else
|
|
97
|
+
val.to_s
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_value"
|
|
4
|
+
|
|
5
|
+
module Unibuf
|
|
6
|
+
module Models
|
|
7
|
+
module Values
|
|
8
|
+
# Represents a nested message value
|
|
9
|
+
# Delegates to Message model for message-specific behavior
|
|
10
|
+
class MessageValue < BaseValue
|
|
11
|
+
attr_reader :message
|
|
12
|
+
|
|
13
|
+
def initialize(raw_value)
|
|
14
|
+
super
|
|
15
|
+
@message = Message.new(raw_value)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def message?
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Delegation to message
|
|
23
|
+
def fields
|
|
24
|
+
message.fields
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def field_count
|
|
28
|
+
message.field_count
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def find_field(name)
|
|
32
|
+
message.find_field(name)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def field_names
|
|
36
|
+
message.field_names
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Serialization - delegates to message
|
|
40
|
+
def to_textproto(indent: 0)
|
|
41
|
+
indent_str = " " * indent
|
|
42
|
+
nested_content = message.to_textproto(indent: indent + 1)
|
|
43
|
+
"{\n#{nested_content}\n#{indent_str}}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Validation
|
|
47
|
+
def validate!
|
|
48
|
+
unless raw_value.is_a?(Hash) && raw_value.key?("fields")
|
|
49
|
+
raise InvalidValueError,
|
|
50
|
+
"MessageValue requires hash with 'fields' key"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
message.validate! if message.respond_to?(:validate!)
|
|
54
|
+
true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Deep equality
|
|
58
|
+
def ==(other)
|
|
59
|
+
return false unless other.is_a?(MessageValue)
|
|
60
|
+
|
|
61
|
+
message == other.message
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def hash
|
|
65
|
+
[self.class, message].hash
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_value"
|
|
4
|
+
|
|
5
|
+
module Unibuf
|
|
6
|
+
module Models
|
|
7
|
+
module Values
|
|
8
|
+
# Scalar value types: string, integer, float, boolean
|
|
9
|
+
# Immutable value object with type coercion
|
|
10
|
+
class ScalarValue < BaseValue
|
|
11
|
+
def scalar?
|
|
12
|
+
true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Type queries - MECE
|
|
16
|
+
def string?
|
|
17
|
+
raw_value.is_a?(String)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def integer?
|
|
21
|
+
raw_value.is_a?(Integer)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def float?
|
|
25
|
+
raw_value.is_a?(Float)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def boolean?
|
|
29
|
+
raw_value.is_a?(TrueClass) || raw_value.is_a?(FalseClass)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def nil?
|
|
33
|
+
raw_value.nil?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Type coercion with validation
|
|
37
|
+
def as_string
|
|
38
|
+
raw_value.to_s
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def as_integer
|
|
42
|
+
return raw_value if integer?
|
|
43
|
+
return raw_value.to_i if string? && raw_value.match?(/^-?\d+$/)
|
|
44
|
+
|
|
45
|
+
raise TypeCoercionError,
|
|
46
|
+
"Cannot convert #{raw_value.class} to Integer"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def as_float
|
|
50
|
+
return raw_value if float?
|
|
51
|
+
return raw_value.to_f if integer?
|
|
52
|
+
return raw_value.to_f if string? && raw_value.match?(/^-?\d+\.?\d*$/)
|
|
53
|
+
|
|
54
|
+
raise TypeCoercionError, "Cannot convert #{raw_value.class} to Float"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def as_boolean
|
|
58
|
+
return raw_value if boolean?
|
|
59
|
+
return true if string? && %w[true t 1].include?(raw_value.downcase)
|
|
60
|
+
return false if string? && %w[false f 0].include?(raw_value.downcase)
|
|
61
|
+
return true if raw_value == 1
|
|
62
|
+
return false if raw_value.zero?
|
|
63
|
+
|
|
64
|
+
raise TypeCoercionError,
|
|
65
|
+
"Cannot convert #{raw_value.class} to Boolean"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Serialization
|
|
69
|
+
def to_textproto(indent: 0)
|
|
70
|
+
format_scalar(raw_value)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Validation
|
|
74
|
+
def validate!
|
|
75
|
+
unless [String, Integer, Float, TrueClass, FalseClass,
|
|
76
|
+
NilClass].any? do |t|
|
|
77
|
+
raw_value.is_a?(t)
|
|
78
|
+
end
|
|
79
|
+
raise InvalidValueError, "Invalid scalar type: #{raw_value.class}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
true
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def format_scalar(value)
|
|
88
|
+
case value
|
|
89
|
+
when String
|
|
90
|
+
escape_string(value)
|
|
91
|
+
when Integer, Float
|
|
92
|
+
value.to_s
|
|
93
|
+
when TrueClass, FalseClass
|
|
94
|
+
value.to_s
|
|
95
|
+
when NilClass
|
|
96
|
+
'""'
|
|
97
|
+
else
|
|
98
|
+
value.to_s
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def escape_string(str)
|
|
103
|
+
escaped = str.gsub("\\", "\\\\")
|
|
104
|
+
.gsub('"', '\"')
|
|
105
|
+
.gsub("\n", '\\n')
|
|
106
|
+
.gsub("\t", '\\t')
|
|
107
|
+
.gsub("\r", '\\r')
|
|
108
|
+
"\"#{escaped}\""
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Unibuf
|
|
4
|
+
module Parsers
|
|
5
|
+
module Binary
|
|
6
|
+
# Binary Protocol Buffer wire format parser
|
|
7
|
+
# Requires bindata gem for implementation
|
|
8
|
+
#
|
|
9
|
+
# TODO: Implement wire format parsing using bindata
|
|
10
|
+
# Reference: https://protobuf.dev/programming-guides/encoding/
|
|
11
|
+
class WireFormatParser
|
|
12
|
+
attr_reader :schema
|
|
13
|
+
|
|
14
|
+
def initialize(schema)
|
|
15
|
+
@schema = schema
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def parse(binary_data)
|
|
19
|
+
raise NotImplementedError, <<~MSG
|
|
20
|
+
Binary Protocol Buffer parsing not yet implemented.
|
|
21
|
+
|
|
22
|
+
This feature requires:
|
|
23
|
+
1. bindata gem integration
|
|
24
|
+
2. Wire format decoder
|
|
25
|
+
3. Schema-driven field extraction
|
|
26
|
+
4. Type deserialization
|
|
27
|
+
|
|
28
|
+
Current implementation: Text format only
|
|
29
|
+
Roadmap: Binary support in v2.0.0
|
|
30
|
+
|
|
31
|
+
For now, use text format:
|
|
32
|
+
Unibuf.parse_textproto(text_content)
|
|
33
|
+
Unibuf.parse_textproto_file("file.txtpb")
|
|
34
|
+
MSG
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def parse_file(path)
|
|
38
|
+
parse(File.binread(path))
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "parslet"
|
|
4
|
+
|
|
5
|
+
module Unibuf
|
|
6
|
+
module Parsers
|
|
7
|
+
module Proto3
|
|
8
|
+
# Parslet grammar for parsing Proto3 schema definitions
|
|
9
|
+
# Reference: https://protobuf.dev/reference/protobuf/proto3-spec/
|
|
10
|
+
class Grammar < Parslet::Parser
|
|
11
|
+
# ===== Lexical Elements =====
|
|
12
|
+
|
|
13
|
+
# Whitespace and comments
|
|
14
|
+
rule(:space) { match['\s'].repeat(1) }
|
|
15
|
+
rule(:space?) { space.maybe }
|
|
16
|
+
rule(:newline) { str("\n") }
|
|
17
|
+
|
|
18
|
+
# Comments (// and /* */)
|
|
19
|
+
rule(:line_comment) do
|
|
20
|
+
str("//") >> (newline.absent? >> any).repeat >> newline.maybe
|
|
21
|
+
end
|
|
22
|
+
rule(:block_comment) do
|
|
23
|
+
str("/*") >> (str("*/").absent? >> any).repeat >> str("*/")
|
|
24
|
+
end
|
|
25
|
+
rule(:comment) { line_comment | block_comment }
|
|
26
|
+
|
|
27
|
+
rule(:whitespace) { (space | comment).repeat(1) }
|
|
28
|
+
rule(:whitespace?) { (space | comment).repeat }
|
|
29
|
+
|
|
30
|
+
# Identifiers
|
|
31
|
+
rule(:letter) { match["a-zA-Z_"] }
|
|
32
|
+
rule(:digit) { match["0-9"] }
|
|
33
|
+
rule(:identifier) do
|
|
34
|
+
(letter >> (letter | digit).repeat).as(:identifier)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Strings
|
|
38
|
+
rule(:string_content) { (str('"').absent? >> any).repeat }
|
|
39
|
+
rule(:string_literal) do
|
|
40
|
+
str('"') >> string_content.as(:string) >> str('"')
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Numbers
|
|
44
|
+
rule(:number) { (match["+-"].maybe >> digit.repeat(1)).as(:number) }
|
|
45
|
+
|
|
46
|
+
# ===== Syntax Elements =====
|
|
47
|
+
|
|
48
|
+
# Syntax declaration: syntax = "proto3";
|
|
49
|
+
rule(:syntax_stmt) do
|
|
50
|
+
str("syntax") >> whitespace? >> str("=") >> whitespace? >>
|
|
51
|
+
string_literal.as(:syntax_version) >> whitespace? >> str(";")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Package declaration: package google.fonts;
|
|
55
|
+
rule(:package_stmt) do
|
|
56
|
+
str("package") >> whitespace? >>
|
|
57
|
+
identifier.as(:package) >>
|
|
58
|
+
(str(".") >> identifier).repeat >> whitespace? >> str(";")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Import statement: import "other.proto";
|
|
62
|
+
rule(:import_stmt) do
|
|
63
|
+
str("import") >> whitespace? >>
|
|
64
|
+
string_literal.as(:import) >> whitespace? >> str(";")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Field types
|
|
68
|
+
rule(:scalar_type) do
|
|
69
|
+
(str("double") | str("float") | str("int32") | str("int64") |
|
|
70
|
+
str("uint32") | str("uint64") | str("sint32") | str("sint64") |
|
|
71
|
+
str("fixed32") | str("fixed64") | str("sfixed32") | str("sfixed64") |
|
|
72
|
+
str("bool") | str("string") | str("bytes")).as(:scalar_type)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
rule(:message_type) { identifier.as(:message_type) }
|
|
76
|
+
rule(:field_type) { scalar_type | message_type }
|
|
77
|
+
|
|
78
|
+
# Field definition: string name = 1;
|
|
79
|
+
rule(:field_def) do
|
|
80
|
+
(str("repeated") >> whitespace).maybe.as(:repeated) >>
|
|
81
|
+
field_type.as(:type) >> whitespace >>
|
|
82
|
+
identifier.as(:name) >> whitespace? >>
|
|
83
|
+
str("=") >> whitespace? >>
|
|
84
|
+
number.as(:field_number) >> whitespace? >>
|
|
85
|
+
str(";")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Map field: map<string, int32> mapping = 1;
|
|
89
|
+
rule(:map_field) do
|
|
90
|
+
str("map") >> whitespace? >> str("<") >> whitespace? >>
|
|
91
|
+
field_type.as(:key_type) >> whitespace? >>
|
|
92
|
+
str(",") >> whitespace? >>
|
|
93
|
+
field_type.as(:value_type) >> whitespace? >>
|
|
94
|
+
str(">") >> whitespace >>
|
|
95
|
+
identifier.as(:name) >> whitespace? >>
|
|
96
|
+
str("=") >> whitespace? >>
|
|
97
|
+
number.as(:field_number) >> whitespace? >>
|
|
98
|
+
str(";")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Message definition
|
|
102
|
+
rule(:message_body) do
|
|
103
|
+
(field_def.as(:field) | map_field.as(:map_field) | message_def.as(:nested_message) | whitespace).repeat
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
rule(:message_def) do
|
|
107
|
+
str("message") >> whitespace >>
|
|
108
|
+
identifier.as(:message_name) >> whitespace? >>
|
|
109
|
+
str("{") >> whitespace? >>
|
|
110
|
+
message_body.as(:body) >> whitespace? >>
|
|
111
|
+
str("}")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Enum definition
|
|
115
|
+
rule(:enum_value) do
|
|
116
|
+
identifier.as(:name) >> whitespace? >>
|
|
117
|
+
str("=") >> whitespace? >>
|
|
118
|
+
number.as(:value) >> whitespace? >>
|
|
119
|
+
str(";")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
rule(:enum_def) do
|
|
123
|
+
str("enum") >> whitespace >>
|
|
124
|
+
identifier.as(:enum_name) >> whitespace? >>
|
|
125
|
+
str("{") >> whitespace? >>
|
|
126
|
+
enum_value.repeat.as(:values) >> whitespace? >>
|
|
127
|
+
str("}")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Top-level elements
|
|
131
|
+
rule(:proto_element) do
|
|
132
|
+
syntax_stmt.as(:syntax) |
|
|
133
|
+
package_stmt.as(:package) |
|
|
134
|
+
import_stmt.as(:import) |
|
|
135
|
+
message_def.as(:message) |
|
|
136
|
+
enum_def.as(:enum) |
|
|
137
|
+
whitespace
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Proto file
|
|
141
|
+
rule(:proto_file) do
|
|
142
|
+
whitespace? >> proto_element.repeat >> whitespace?
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
root(:proto_file)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../models/schema"
|
|
4
|
+
require_relative "../../models/message_definition"
|
|
5
|
+
require_relative "../../models/field_definition"
|
|
6
|
+
require_relative "../../models/enum_definition"
|
|
7
|
+
|
|
8
|
+
module Unibuf
|
|
9
|
+
module Parsers
|
|
10
|
+
module Proto3
|
|
11
|
+
# Processor to transform Proto3 AST to Schema models
|
|
12
|
+
class Processor
|
|
13
|
+
class << self
|
|
14
|
+
def process(ast)
|
|
15
|
+
return Models::Schema.new unless ast
|
|
16
|
+
|
|
17
|
+
elements = Array(ast)
|
|
18
|
+
|
|
19
|
+
attributes = {
|
|
20
|
+
syntax: extract_syntax(elements),
|
|
21
|
+
package: extract_package(elements),
|
|
22
|
+
imports: extract_imports(elements),
|
|
23
|
+
messages: extract_messages(elements),
|
|
24
|
+
enums: extract_enums(elements),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
Models::Schema.new(attributes)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def extract_syntax(elements)
|
|
33
|
+
syntax_element = elements.find { |el| el.key?(:syntax) }
|
|
34
|
+
return "proto3" unless syntax_element
|
|
35
|
+
|
|
36
|
+
syntax_element[:syntax][:syntax_version][:string].to_s
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def extract_package(elements)
|
|
40
|
+
pkg_element = elements.find { |el| el.key?(:package) }
|
|
41
|
+
return nil unless pkg_element
|
|
42
|
+
|
|
43
|
+
# Package is array of identifiers: [{:package=>{:identifier=>"google"}}, {:identifier=>"fonts"}]
|
|
44
|
+
pkg_parts = Array(pkg_element[:package])
|
|
45
|
+
names = pkg_parts.filter_map do |part|
|
|
46
|
+
if part[:package]
|
|
47
|
+
part[:package][:identifier].to_s
|
|
48
|
+
elsif part[:identifier]
|
|
49
|
+
part[:identifier].to_s
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
names.join(".")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def extract_imports(elements)
|
|
57
|
+
elements.select { |el| el.key?(:import) }.map do |el|
|
|
58
|
+
el[:import][:import][:string].to_s
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def extract_messages(elements)
|
|
63
|
+
elements.select { |el| el.key?(:message) }.map do |el|
|
|
64
|
+
process_message(el[:message])
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def extract_enums(elements)
|
|
69
|
+
elements.select { |el| el.key?(:enum) }.map do |el|
|
|
70
|
+
process_enum(el[:enum])
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def process_message(msg_data)
|
|
75
|
+
name = msg_data[:message_name][:identifier].to_s
|
|
76
|
+
body = msg_data[:body]
|
|
77
|
+
|
|
78
|
+
fields = extract_fields(body)
|
|
79
|
+
nested_messages = extract_nested_messages(body)
|
|
80
|
+
nested_enums = extract_nested_enums(body)
|
|
81
|
+
|
|
82
|
+
Models::MessageDefinition.new(
|
|
83
|
+
name: name,
|
|
84
|
+
fields: fields,
|
|
85
|
+
nested_messages: nested_messages,
|
|
86
|
+
nested_enums: nested_enums,
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def extract_fields(body)
|
|
91
|
+
return [] unless body
|
|
92
|
+
|
|
93
|
+
result = []
|
|
94
|
+
|
|
95
|
+
# Extract regular fields
|
|
96
|
+
Array(body).each do |el|
|
|
97
|
+
if el.respond_to?(:key?) && el.key?(:field)
|
|
98
|
+
result << process_field(el[:field])
|
|
99
|
+
elsif el.respond_to?(:key?) && el.key?(:map_field)
|
|
100
|
+
result << process_map_field(el[:map_field])
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
result
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def extract_nested_messages(body)
|
|
108
|
+
return [] unless body
|
|
109
|
+
|
|
110
|
+
Array(body).select do |el|
|
|
111
|
+
el.respond_to?(:key?) && el.key?(:nested_message)
|
|
112
|
+
end.map do |el|
|
|
113
|
+
process_message(el[:nested_message])
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def extract_nested_enums(body)
|
|
118
|
+
return [] unless body
|
|
119
|
+
|
|
120
|
+
Array(body).select do |el|
|
|
121
|
+
el.respond_to?(:key?) && el.key?(:enum)
|
|
122
|
+
end.map do |el|
|
|
123
|
+
process_enum(el[:enum])
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def process_field(field_data)
|
|
128
|
+
type_info = field_data[:type]
|
|
129
|
+
type = if type_info[:scalar_type]
|
|
130
|
+
type_info[:scalar_type].to_s
|
|
131
|
+
else
|
|
132
|
+
type_info[:message_type][:identifier].to_s
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
Models::FieldDefinition.new(
|
|
136
|
+
name: field_data[:name][:identifier].to_s,
|
|
137
|
+
type: type,
|
|
138
|
+
number: field_data[:field_number][:number].to_s.to_i,
|
|
139
|
+
label: field_data[:repeated] ? "repeated" : nil,
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def process_map_field(map_data)
|
|
144
|
+
key_type_info = map_data[:key_type]
|
|
145
|
+
key_type = if key_type_info[:scalar_type]
|
|
146
|
+
key_type_info[:scalar_type].to_s
|
|
147
|
+
else
|
|
148
|
+
key_type_info[:message_type][:identifier].to_s
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
value_type_info = map_data[:value_type]
|
|
152
|
+
value_type = if value_type_info[:scalar_type]
|
|
153
|
+
value_type_info[:scalar_type].to_s
|
|
154
|
+
else
|
|
155
|
+
value_type_info[:message_type][:identifier].to_s
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
Models::FieldDefinition.new(
|
|
159
|
+
name: map_data[:name][:identifier].to_s,
|
|
160
|
+
type: "map",
|
|
161
|
+
number: map_data[:field_number][:number].to_s.to_i,
|
|
162
|
+
key_type: key_type,
|
|
163
|
+
value_type: value_type,
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def process_enum(enum_data)
|
|
168
|
+
name = enum_data[:enum_name][:identifier].to_s
|
|
169
|
+
values = {}
|
|
170
|
+
|
|
171
|
+
Array(enum_data[:values]).each do |val|
|
|
172
|
+
next unless val.respond_to?(:key?) && val.key?(:name)
|
|
173
|
+
|
|
174
|
+
val_name = val[:name][:identifier].to_s
|
|
175
|
+
val_num = val[:value][:number].to_s.to_i
|
|
176
|
+
values[val_name] = val_num
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
Models::EnumDefinition.new(
|
|
180
|
+
name: name,
|
|
181
|
+
values: values,
|
|
182
|
+
)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|