protod 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,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