protod 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Protod
4
+ module Proto
5
+ class Builder
6
+ def initialize(root_package)
7
+ @root_package = root_package
8
+ @receivers = {}
9
+ end
10
+
11
+ def push_receiver(ruby_ident)
12
+ @receivers[ruby_ident.to_s] = true
13
+ end
14
+
15
+ def receiver_pushed?(ruby_ident)
16
+ @receivers.key?(ruby_ident.to_s)
17
+ end
18
+
19
+ def build
20
+ return if @root_package.built?
21
+
22
+ handler = Protod::Rpc::Handler.build_in(@root_package)
23
+
24
+ @receivers.keys.each do |receiver|
25
+ package = Protod.find_or_register_package("#{@root_package.full_ident}.#{receiver.underscore.gsub('/', '.')}")
26
+
27
+ request_field = Protod::Proto::Field.build_from(Protod::Rpc::Request.find_by(receiver), ident: receiver)
28
+ response_field = Protod::Proto::Field.build_from(Protod::Rpc::Response.find_by(receiver), ident: receiver)
29
+
30
+ handler.register_receiver(request_field, response_field)
31
+
32
+ request_message = package.bind(request_field.interpreter)
33
+ response_message = package.bind(response_field.interpreter)
34
+
35
+ bindable_interpreters_under(request_message, response_message).each do |i|
36
+ root_message_name = i.const.ruby_ident.singleton ? 'Singleton' : 'Instance'
37
+
38
+ package
39
+ .find_or_push(root_message_name, by: :ident, into: :messages)
40
+ .find_or_push(i.const.ruby_ident.method_name, by: :ident, into: :messages)
41
+ .bind(i)
42
+ end
43
+ end
44
+
45
+ models_package = Protod.find_or_register_package("#{@root_package.full_ident}.models")
46
+
47
+ while (interpreters = bindable_interpreters_under(*@root_package.all_packages)).present?
48
+ interpreters.each { models_package.bind(_1) }
49
+ end
50
+
51
+ # For supporting an Array/Hash instance at the fields whiches type is google.protobuf.Any,
52
+ # make the proto definition for Array emerge even if it's not requried.
53
+ any_interpreter = Protod::Interpreter.find_by('RBS::Types::Bases::Any')
54
+ if @root_package.all_packages.flat_map(&:collect_fields).any? { _1.interpreter == any_interpreter }
55
+ i = Protod::Interpreter.find_by('Array')
56
+
57
+ models_package.bind(i) if i.bindable?
58
+
59
+ models_package.imports.push(Protod::Interpreter.find_by('Hash').proto_path)
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def bindable_interpreters_under(*parts)
66
+ parts.flat_map(&:collect_fields).filter_map(&:interpreter).uniq.select(&:bindable?)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Protod
4
+ module Proto
5
+ class << self
6
+ # https://github.com/protocolbuffers/protobuf/blob/v3.12.0/docs/field_presence.md
7
+ def omits_field?(pb, name)
8
+ return false unless pb.respond_to?("has_#{name}?")
9
+ return false if pb.public_send("has_#{name}?")
10
+ true
11
+ end
12
+ end
13
+
14
+ class Ident < ::String
15
+ class << self
16
+ def build_from(const_name)
17
+ return if const_name.blank?
18
+
19
+ new(const_name)
20
+ end
21
+ end
22
+
23
+ attr_reader :const_name
24
+
25
+ def initialize(const_name)
26
+ @const_name = Protod::RubyIdent.absolute_of(const_name)
27
+
28
+ super(const_name.gsub('::', '__').delete_prefix('__'))
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Protod
4
+ module Proto
5
+ class Field < Part
6
+ attribute :interpreter
7
+ attribute :as_keyword, :boolean, default: false
8
+ attribute :as_rest, :boolean, default: false
9
+ attribute :required, :boolean, default: true # whether to be not able to omit in Ruby
10
+ attribute :optional, :boolean, default: false # whether to be optional in proto
11
+ attribute :repeated, :boolean, default: false
12
+
13
+ class << self
14
+ def build_from(const_or_name, **attributes)
15
+ i = Protod::Interpreter.find_by(const_or_name, with_register_from_ancestor: true)
16
+
17
+ raise NotImplementedError, "Not found the interpreter for #{const_or_name}. You can define a interpreter using Protod::Interpreter.register_for" unless i
18
+
19
+ new(interpreter: i, **attributes)
20
+ end
21
+
22
+ def build_from_rbs(type, on:, **attributes)
23
+ case type
24
+ when RBS::Types::Optional
25
+ build_from_rbs(type.type, on: on, **attributes.merge(optional: attributes[:repeated] ? false : true))
26
+ when RBS::Types::Union
27
+ real_type = type.types.find { _1.name.kind == :class && Protod.rbs_environment.class_decls.key?(_1.name) }
28
+
29
+ raise ArgumentError, "Not found declared class in union type on #{on}" unless real_type
30
+
31
+ build_from_rbs(real_type, on: on, **attributes)
32
+ when RBS::Types::Alias
33
+ alias_decl = Protod.rbs_environment.type_alias_decls[type.name]
34
+
35
+ raise ArgumentError, "Not found alias declaration of #{type.name.name} on #{on}" unless alias_decl
36
+
37
+ build_from_rbs(alias_decl.decl.type, on: on, **attributes)
38
+ when RBS::Types::ClassInstance
39
+ case
40
+ when should_repeated_with(type.name.to_s.safe_constantize)
41
+ build_from_rbs(type.args.first, on: on, **attributes.merge(optional: false, repeated: true))
42
+ when type.args.size > 0
43
+ raise NotImplementedError, "Unsupported rbs type : Record or Tuple on #{on}"
44
+ else
45
+ build_from(type.name.to_s, **attributes)
46
+ end
47
+ when RBS::Types::Bases::Base
48
+ build_from(type.class.name, **attributes)
49
+ else
50
+ binding.pry
51
+ raise NotImplementedError, "Unsupported rbs type : #{type.class.name} on #{on}"
52
+ end
53
+ end
54
+
55
+ def should_repeated_with(const)
56
+ const&.ancestors&.include?(::Array)
57
+ end
58
+ end
59
+
60
+ def void?
61
+ interpreter ? interpreter.proto_ident.blank? : false
62
+ end
63
+
64
+ def to_proto
65
+ raise ArgumentError, "Not set interpreter" unless interpreter
66
+
67
+ type_part = if interpreter.package && interpreter.package == ancestor_as(Protod::Proto::Package)
68
+ interpreter.proto_full_ident.delete_prefix("#{interpreter.package.full_ident}.")
69
+ else
70
+ interpreter.proto_full_ident
71
+ end
72
+
73
+ [
74
+ format_proto(''),
75
+ [
76
+ # optional ? 'optional' : nil,
77
+ repeated ? 'repeated' : nil,
78
+ type_part,
79
+ ident,
80
+ '=',
81
+ index
82
+ ].compact.join(' '),
83
+ ';'
84
+ ].join
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Protod
4
+ module Proto
5
+ class Message < Part
6
+ include Findable
7
+ include FieldCollectable
8
+ include FieldNumeringable
9
+ include InterpreterBindable
10
+
11
+ attribute :messages, default: -> { [] }
12
+ attribute :fields, default: -> { [] }
13
+
14
+ findable_callback_for(:message, key: [:ident, :ruby_ident]) do |key, value|
15
+ @message_map ? @message_map.fetch(key)[value] : messages.find { _1.public_send(key) == value }
16
+ end
17
+
18
+ findable_callback_for(:field, :oneof, key: :ident) do |key, value|
19
+ @field_map ? @field_map.fetch(key)[value] : fields.find { _1.public_send(key) == value }
20
+ end
21
+
22
+ def has?(part, in_the:)
23
+ case in_the
24
+ when :fields
25
+ idents = fields.flat_map do |f|
26
+ case f
27
+ when Protod::Proto::Field
28
+ [f.ident]
29
+ when Protod::Proto::Oneof
30
+ [f.ident, *f.fields.map(&:ident)]
31
+ else
32
+ raise ArgumentError, "Unacceptable field : #{f.ident} of #{f.class.name}"
33
+ end
34
+ end
35
+
36
+ part.ident.in?(idents)
37
+ else
38
+ super
39
+ end
40
+ end
41
+
42
+ def ruby_ident
43
+ ident.const_name
44
+ end
45
+
46
+ def full_ident
47
+ [parent&.full_ident, ident].compact.join('.').presence
48
+ end
49
+
50
+ def pb_const
51
+ raise NotImplementedError, "Can't call pb_const for #{ident} : not set parent yet" unless parent
52
+
53
+ Google::Protobuf::DescriptorPool.generated_pool.lookup(full_ident).msgclass
54
+ end
55
+
56
+ def freeze
57
+ messages.each { _1.depth = depth + 1 }
58
+ fields.each { _1.depth = depth + 1 }
59
+
60
+ messages.each.with_index(1) { |m, i| m.index = i }
61
+ numbering_fields_with(1)
62
+
63
+ @message_map = self.class.findable_keys_for(:message).index_with { |k| messages.index_by(&k.to_sym) }
64
+ @field_map = self.class.findable_keys_for(:field).index_with { |k| fields.index_by(&k.to_sym) }
65
+
66
+ super
67
+ end
68
+
69
+ def to_proto
70
+ message_part = messages.map { _1.to_proto }.join("\n\n").presence
71
+
72
+ field_part = fields.filter_map do |f|
73
+ case f
74
+ when Protod::Proto::Field
75
+ next if f.void?
76
+
77
+ f.to_proto
78
+ when Protod::Proto::Oneof
79
+ f.to_proto
80
+ else
81
+ raise NotImplementedError, "Sorry, this is bug forgetting to implement for #{f.class.name}"
82
+ end
83
+ end.join("\n").presence
84
+
85
+ body = [message_part, field_part].compact.join("\n\n").presence
86
+ body = "\n#{body}\n" if body
87
+
88
+ [
89
+ format_proto("message %s {%s", ident, body),
90
+ format_proto("}")
91
+ ].join
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Protod
4
+ module Proto
5
+ class Oneof < Part
6
+ include Findable
7
+ include FieldNumeringable
8
+
9
+ attribute :fields, default: -> { [] }
10
+
11
+ findable_callback_for(:field, :oneof, key: :ident) do |key, value|
12
+ @field_map ? @field_map.fetch(key)[value] : fields.find { _1.public_send(key) == value }
13
+ end
14
+
15
+ def freeze
16
+ fields.each { _1.depth = depth + 1 }
17
+
18
+ @field_map = self.class.findable_keys_for(:field).index_with { |k| fields.index_by(&k.to_sym) }
19
+
20
+ super
21
+ end
22
+
23
+ def to_proto
24
+ field_part = fields.filter_map do |f|
25
+ case f
26
+ when Protod::Proto::Field
27
+ next if f.void?
28
+
29
+ f.to_proto
30
+ when Protod::Proto::Oneof
31
+ f.to_proto
32
+ else
33
+ raise NotImplementedError, "Sorry, this is bug forgetting to implement for #{f.class.name}"
34
+ end
35
+ end.join("\n").presence
36
+
37
+ field_part = "\n#{field_part}\n" if field_part
38
+
39
+ [
40
+ format_proto("oneof %s {%s", ident, field_part),
41
+ format_proto("}")
42
+ ].join
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Protod
4
+ module Proto
5
+ class Package < Part
6
+ include Findable
7
+ include FieldCollectable
8
+ include InterpreterBindable
9
+
10
+ attribute :url
11
+ attribute :branch
12
+ attribute :for_ruby, :string
13
+ attribute :for_java, :string
14
+ attribute :packages, default: -> { [] }
15
+ attribute :services, default: -> { [] }
16
+ attribute :messages, default: -> { [] }
17
+ attribute :imports, default: -> { [] }
18
+
19
+ findable_callback_for(:package, key: :full_ident) do |key, value|
20
+ @package_map ? @package_map.fetch(key)[value] : all_packages.find { _1.public_send(key) == value }
21
+ end
22
+
23
+ findable_callback_for(:service, key: [:ident, :ruby_ident]) do |key, value|
24
+ @service_map ? @service_map.fetch(key)[value] : services.find { _1.public_send(key) == value }
25
+ end
26
+
27
+ findable_callback_for(:message, key: [:ident, :ruby_ident]) do |key, value|
28
+ @message_map ? @message_map.fetch(key)[value] : messages.find { _1.public_send(key) == value }
29
+ end
30
+
31
+ class << self
32
+ def clear!
33
+ @packages = nil
34
+ end
35
+
36
+ def roots
37
+ @packages ||= []
38
+ end
39
+
40
+ def find_or_register_package(full_ident, **attributes)
41
+ full_ident.split('.').inject(nil) do |parent, ident|
42
+ current_packages = parent ? parent.packages : roots
43
+
44
+ current_packages.find { _1.ident == ident } || new.tap do
45
+ _1.assign_attributes(
46
+ parent: parent,
47
+ ident: ident,
48
+ for_ruby: parent&.for_ruby && "#{parent.for_ruby}::#{ident.camelize}",
49
+ for_java: parent&.for_java && "#{parent.for_java}.#{ident}"
50
+ )
51
+
52
+ current_packages.push(_1)
53
+ end
54
+ end.tap do
55
+ _1.assign_attributes(**attributes.compact) if attributes.compact.present?
56
+ end
57
+ end
58
+ end
59
+
60
+ def proto_path
61
+ full_ident.gsub('.', '/').then { "#{_1}.proto" }
62
+ end
63
+
64
+ def full_ident
65
+ [parent&.full_ident, ident].compact.join('.').presence if ident
66
+ end
67
+
68
+ def pb_const
69
+ for_ruby&.constantize || full_ident.split('.').map(&:camelize).join('::').constantize
70
+ end
71
+
72
+ def all_packages
73
+ packages.flat_map(&:all_packages).tap { _1.unshift(self) }
74
+ end
75
+
76
+ def empty?
77
+ services.empty? && messages.empty?
78
+ end
79
+
80
+ def external?
81
+ url.present?
82
+ end
83
+
84
+ def freeze
85
+ services.each.with_index(1) { |s, i| s.index = i }
86
+ messages.each.with_index(1) { |m, i| m.index = i }
87
+
88
+ @package_map = self.class.findable_keys_for(:package).index_with { |k| all_packages.index_by(&k.to_sym) }
89
+ @service_map = self.class.findable_keys_for(:service).index_with { |k| services.index_by(&k.to_sym) }
90
+ @message_map = self.class.findable_keys_for(:message).index_with { |k| messages.index_by(&k.to_sym) }
91
+
92
+ super
93
+ end
94
+
95
+ def to_proto
96
+ syntax_part = format_proto("syntax = \"proto3\";")
97
+
98
+ package_part = format_proto("package %s;", full_ident)
99
+
100
+ option_part = [
101
+ for_ruby ? format_proto("option ruby_package = \"%s\";", for_ruby) : nil,
102
+ for_java ? format_proto("option java_package = \"%s\";", for_java) : nil,
103
+ ].compact.join("\n").presence
104
+
105
+ import_part = [
106
+ *collect_fields.filter_map(&:interpreter).uniq.filter_map(&:proto_path).uniq.reject { _1 == proto_path },
107
+ *imports
108
+ ].uniq.sort.map { format_proto('import "%s";', _1) }.join("\n").presence
109
+
110
+ message_part = messages.map { _1.to_proto }.join("\n\n").presence
111
+ service_part = services.map { _1.to_proto }.join("\n\n").presence
112
+
113
+ [
114
+ [syntax_part, package_part, option_part, import_part, message_part, service_part].compact.join("\n\n"),
115
+ "\n"
116
+ ].join
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Protod
4
+ module Proto
5
+ class Part
6
+ include ActiveModel::Model
7
+ include ActiveModel::Attributes
8
+
9
+ attribute :parent
10
+ attribute :comment, :string
11
+ attribute :ident
12
+ attribute :depth, :integer, default: 0
13
+ attribute :index, :integer, default: 1
14
+
15
+ def ident=(value)
16
+ super(Protod::Proto::Ident.build_from(value.to_s))
17
+
18
+ raise ArgumentError, "Invalid grpc ident : #{value}. see https://protobuf.dev/reference/protobuf/proto3-spec/#identifiers" unless ident&.match(/\A[a-zA-Z][a-zA-Z0-9_]*\z/)
19
+ end
20
+
21
+ def root
22
+ parent ? parent.root : self
23
+ end
24
+
25
+ def ancestor_as(part_const)
26
+ parent.is_a?(part_const) ? parent : parent&.ancestor_as(part_const)
27
+ end
28
+
29
+ def push(part, into:, ignore: false)
30
+ already_pushed = has?(part, in_the: into)
31
+
32
+ raise ArgumentError, "Can't push already present #{part.ident} in #{ident}" if already_pushed && ignore.!
33
+ raise ArgumentError, "Can't push already bound to #{part.parent.ident} in #{ident}" if part.parent
34
+
35
+ part.assign_attributes(parent: self)
36
+
37
+ public_send(into).push(part) unless already_pushed
38
+
39
+ part
40
+ end
41
+
42
+ def has?(part, in_the:)
43
+ public_send(in_the).any? { _1.ident == part.ident }
44
+ end
45
+
46
+ def to_proto
47
+ raise NotImplementedError, "Not defined #{self.class.name}##{__method__}"
48
+ end
49
+
50
+ def freeze
51
+ super.tap do
52
+ (attributes.keys - %w[parent]).each do |attribute_name|
53
+ value = attributes.fetch(attribute_name)
54
+
55
+ value.freeze
56
+
57
+ value.each(&:freeze) if value.is_a?(::Array)
58
+ end
59
+ end
60
+ end
61
+
62
+ alias_method :built?, :frozen?
63
+
64
+ private
65
+
66
+ def format_proto(fmt, *args)
67
+ format("%s#{fmt}", ' ' * depth, *args)
68
+ end
69
+ end
70
+
71
+ module Findable
72
+ extend ActiveSupport::Concern
73
+
74
+ class_methods do
75
+ def findable_callback_for(*part_class_names, key:, &body)
76
+ part_class_names.each do |part_class_name|
77
+ k = "Protod::Proto::#{part_class_name.to_s.classify}"
78
+
79
+ self._findable_keys_for ||= {}
80
+ self._findable_body_for ||= {}
81
+
82
+ self._findable_keys_for[k] = ::Array.wrap(key).map(&:to_s)
83
+ self._findable_body_for[k] = body
84
+ end
85
+ end
86
+
87
+ def findable_keys_for(part_class_name)
88
+ k = "Protod::Proto::#{part_class_name.to_s.classify}"
89
+
90
+ _findable_keys_for[k] || []
91
+ end
92
+ end
93
+
94
+ included do
95
+ class_attribute :_findable_keys_for
96
+ class_attribute :_findable_body_for
97
+
98
+ def find(part, by:, as: nil)
99
+ by = by.to_s
100
+ value = case part
101
+ when ::String
102
+ part
103
+ when ::Symbol
104
+ part.to_s
105
+ else
106
+ part.public_send(by)
107
+ end
108
+ part = as ? as.safe_constantize&.allocate : part
109
+ keys = _findable_keys_for.fetch(part.class.name, nil)
110
+ body = _findable_body_for.fetch(part.class.name, nil)
111
+
112
+ raise ArgumentError, "Unsupported as : #{as}" if keys.blank? && as
113
+ raise ArgumentError, "Unsupported part : #{part.class.name}" if keys.blank?
114
+ raise ArgumentError, "Unsupported by : #{by}. #{keys.join(', ')} are available" unless by.in?(keys)
115
+ raise NotImplementedError, "Sorry, this is bug forgetting to implement for #{part.class.name} at #{self.class.name}" unless body
116
+
117
+ instance_exec(by, value, &body)
118
+ end
119
+
120
+ def find_or_push(part, into:, by:, as: nil, &body)
121
+ new_part = if part.is_a?(::String)
122
+ c = as ? as.safe_constantize : "Protod::Proto::#{into.to_s.classify}".constantize
123
+
124
+ raise ArgumentError, "Unsupported as : #{as}" unless c
125
+
126
+ c.new(by.to_sym => part)
127
+ else
128
+ part
129
+ end
130
+
131
+ as = part.is_a?(::String) ? new_part.class.name : nil
132
+
133
+ find(part, by: by, as: as) || push(new_part, into: into).tap { body&.call(_1) }
134
+ end
135
+ end
136
+ end
137
+
138
+ module FieldCollectable
139
+ def collect_fields
140
+ collector = ->(part) do
141
+ case part
142
+ when Protod::Proto::Package
143
+ part.messages.flat_map { collector.call(_1) }
144
+ when Protod::Proto::Message
145
+ [
146
+ *part.fields.flat_map { collector.call(_1) },
147
+ *part.messages.flat_map { collector.call(_1) },
148
+ ]
149
+ when Protod::Proto::Oneof
150
+ part.fields.flat_map { collector.call(_1) }
151
+ when Protod::Proto::Field
152
+ [part]
153
+ else
154
+ []
155
+ end
156
+ end
157
+
158
+ collector.call(self)
159
+ end
160
+ end
161
+
162
+ module FieldNumeringable
163
+ def numbering_fields_with(start_index)
164
+ index = start_index
165
+
166
+ fields.each do |f|
167
+ case f
168
+ when Protod::Proto::Field
169
+ f.index = index
170
+
171
+ index = f.index + 1
172
+ when Protod::Proto::Oneof
173
+ index = f.numbering_fields_with(index)
174
+ else
175
+ raise NotImplementedError, "Sorry, this is bug forgetting to implement for #{f.class.name}"
176
+ end
177
+ end
178
+
179
+ index
180
+ end
181
+ end
182
+
183
+ module InterpreterBindable
184
+ def bind(interpreter)
185
+ raise ArgumentError, "Not bindable interpreter #{interpreter.proto_full_ident} trying bound to #{ident}" unless interpreter.bindable?
186
+ interpreter.set_parent(self)
187
+ push(interpreter.proto_message, into: :messages, ignore: true)
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Protod
4
+ module Proto
5
+ class Procedure < Part
6
+ attribute :singleton, :boolean, default: false
7
+ attribute :has_request, :boolean, default: true
8
+ attribute :has_response, :boolean, default: true
9
+ attribute :streaming_request, :boolean, default: false
10
+ attribute :streaming_response, :boolean, default: false
11
+
12
+ def ident=(value)
13
+ super(value.to_s.camelize)
14
+ end
15
+
16
+ def ruby_ident
17
+ raise ArgumentError, "Not set parent" unless parent
18
+
19
+ Protod::RubyIdent.new(const_name: parent.ruby_ident, method_name: ruby_method_name, singleton: singleton)
20
+ end
21
+
22
+ def ruby_method_name
23
+ ident.underscore
24
+ end
25
+
26
+ def request_ident
27
+ "#{ident}Request"
28
+ end
29
+
30
+ def response_ident
31
+ "#{ident}Response"
32
+ end
33
+
34
+ def to_proto
35
+ request_part = format("%s%s", streaming_request ? 'stream ' : '', has_request ? request_ident : '')
36
+ response_part = format("%s%s", streaming_response ? 'stream ' : '', has_response ? response_ident : '')
37
+
38
+ format_proto("rpc %s (%s) returns (%s);", ident, request_part, response_part)
39
+ end
40
+ end
41
+ end
42
+ end