protod 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Protod
4
+ module Proto
5
+ class Service < Part
6
+ include Findable
7
+
8
+ attribute :procedures, default: -> { [] }
9
+
10
+ findable_callback_for(:procedure, key: %i[ident ruby_ident ruby_method_name]) do |key, value|
11
+ @procedure_map ? @procedure_map.fetch(key)[value] : procedures.find { _1.public_send(key) == value }
12
+ end
13
+
14
+ def ruby_ident
15
+ ident.const_name
16
+ end
17
+
18
+ def pb_const
19
+ parent.pb_const.const_get(ident).const_get('Service')
20
+ end
21
+
22
+ def freeze
23
+ procedures.each { _1.depth = depth + 1 }
24
+ procedures.each.with_index(1) { |p, i| p.index = i }
25
+
26
+ @procedure_map = self.class.findable_keys_for(:procedure).index_with { |k| procedures.index_by(&k.to_sym) }
27
+
28
+ super
29
+ end
30
+
31
+ def to_proto
32
+ procedure_part = procedures.map { _1.to_proto }.join("\n").presence
33
+ procedure_part = "\n#{procedure_part}\n" if procedure_part
34
+
35
+ [
36
+ format_proto("service %s {%s", ident, procedure_part),
37
+ format_proto("}")
38
+ ].join
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,46 @@
1
+ Protod.setup!
2
+
3
+ pb_dir = File.absolute_path(Protod.configuration.pb_root_dir)
4
+
5
+ $LOAD_PATH.unshift(pb_dir) unless $LOAD_PATH.include?(pb_dir)
6
+
7
+ Protod::Proto::Package.roots.flat_map(&:all_packages)
8
+ .reject(&:external?)
9
+ .reject { _1.services.blank? }
10
+ .each do |package|
11
+ dirs = package.full_ident.split('.')
12
+ file = dirs.pop
13
+
14
+ require Pathname.new(pb_dir).join(*dirs, "#{file}_services_pb")
15
+ end
16
+
17
+ require 'google/protobuf/well_known_types'
18
+
19
+ class Protod
20
+ module ProtocolBuffers
21
+ module GoogleProtobufStructStringified
22
+ def from_hash(hash)
23
+ super(hash.stringify_keys)
24
+ end
25
+ end
26
+
27
+ module GoogleProtobufValueStringified
28
+ def from_ruby(value)
29
+ case value
30
+ when ::Symbol
31
+ self.string_value = value.to_s
32
+ else
33
+ super(value)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ class Google::Protobuf::Struct
41
+ class << self
42
+ prepend Protod::ProtocolBuffers::GoogleProtobufStructStringified
43
+ end
44
+ end
45
+
46
+ Google::Protobuf::Value.prepend Protod::ProtocolBuffers::GoogleProtobufValueStringified
@@ -0,0 +1,91 @@
1
+ require 'rake'
2
+ require 'rake/tasklib'
3
+
4
+ class Protod
5
+ class RakeTask < Rake::TaskLib
6
+ class Builder
7
+ attr_accessor :name
8
+
9
+ def initialize(&body)
10
+ body&.call(self)
11
+ end
12
+
13
+ def build
14
+ Protod::RakeTask.new(**{ name: name }.compact)
15
+ end
16
+ end
17
+
18
+ def initialize(name: :protod)
19
+ super()
20
+
21
+ @name = name
22
+
23
+ define_generate_proto_task
24
+ define_generate_pb_task
25
+ define_run_gruf
26
+ end
27
+
28
+ def define_generate_proto_task
29
+ desc 'Generate proto files'
30
+ task("#{@name}:generate:proto": :environment) do
31
+ Protod.setup!
32
+
33
+ root = Pathname.new(Protod.configuration.proto_root_dir)
34
+
35
+ Protod::Proto::Package.roots.flat_map { _1.all_packages.reject(&:external?).reject(&:empty?) }.each do |package|
36
+ path = root.join(*package.full_ident.split('.').tap { _1.last << '.proto' })
37
+
38
+ FileUtils.mkdir_p(path.parent) unless File.exist?(path.parent)
39
+
40
+ puts "Start to generate #{path} ..."
41
+ File.write(path, package.to_proto)
42
+ end
43
+ end
44
+ end
45
+
46
+ def define_generate_pb_task
47
+ desc 'Generate protocol buffers files'
48
+ task("#{@name}:generate:pb": :environment) do
49
+ Protod.setup!
50
+
51
+ proto_dir = Pathname.new(Protod.configuration.proto_root_dir)
52
+ pb_dir = File.absolute_path(Pathname.new(Protod.configuration.pb_root_dir))
53
+
54
+ FileUtils.mkdir_p(pb_dir) unless File.exist?(pb_dir)
55
+
56
+ Dir.mktmpdir do |dir|
57
+ dir = Pathname(dir)
58
+
59
+ Protod::Proto::Package.roots.flat_map(&:all_packages).filter { _1.url.present? }.each.with_index(1) do |package, i|
60
+ args = [package.url, dir.join("_ext#{i}_#{package.url.split('/').last}")]
61
+ options = { depth: 1, branch: package.branch }.compact
62
+
63
+ option_part = options.map { |k, v| "--#{k} #{v}" }.join(' ')
64
+ arg_part = args.map { Shellwords.shellescape(_1) }.join(' ')
65
+ cmd = "git clone #{option_part} #{arg_part}"
66
+
67
+ puts "#{cmd}"
68
+ system(cmd) or raise "Failed to generate pb!"
69
+ end
70
+
71
+ include_option = Dir.glob("#{dir}/*").map { "-I#{_1}" }.join(' ')
72
+ cmd = "bundle exec grpc_tools_ruby_protoc #{include_option} -I#{proto_dir} --ruby_out=#{pb_dir} --grpc_out=#{pb_dir} `find #{proto_dir} -type f -name '*.proto'`"
73
+
74
+ puts "#{cmd}"
75
+ system(cmd) or raise "Failed to generate pb!"
76
+ end
77
+
78
+ puts "Finished to generate pb."
79
+ end
80
+ end
81
+
82
+ def define_run_gruf
83
+ desc 'Run gruf'
84
+ task("#{@name}:gruf": :environment) do
85
+ require 'protod/protocol_buffers'
86
+
87
+ ::Gruf::Cli::Executor.new.run
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,114 @@
1
+ class Protod
2
+ module Rpc
3
+ class Handler
4
+ SERVICE_NAME = 'Handler'
5
+ PROCEDURE_NAME = 'handle'
6
+ ONEOF_NAME = 'receiver'
7
+
8
+ class << self
9
+ def build_in(package)
10
+ package
11
+ .find_or_push(SERVICE_NAME, by: :ident, into: :services)
12
+ .find_or_push(Protod::Proto::Procedure.new(ident: PROCEDURE_NAME, streaming_request: true, streaming_response: true), by: :ident, into: :procedures)
13
+ .tap do |procedure|
14
+ package
15
+ .find_or_push(procedure.request_ident, by: :ident, into: :messages)
16
+ .find_or_push(Protod::Proto::Oneof.new(ident: ONEOF_NAME), by: :ident, into: :fields)
17
+
18
+ package
19
+ .find_or_push(procedure.response_ident, by: :ident, into: :messages)
20
+ .find_or_push(Protod::Proto::Oneof.new(ident: ONEOF_NAME), by: :ident, into: :fields)
21
+ end
22
+
23
+ new(package)
24
+ end
25
+
26
+ def find_package
27
+ Protod::Proto::Package.roots.flat_map(&:all_packages).find do
28
+ _1.find(SERVICE_NAME, by: :ident, as: 'Protod::Proto::Service')
29
+ &.find(Protod::Proto::Procedure.new(ident: PROCEDURE_NAME), by: :ident)
30
+ end
31
+ end
32
+
33
+ def find_service_in(package)
34
+ package.find(Protod::Rpc::Handler::SERVICE_NAME, by: :ident, as: 'Protod::Proto::Service')
35
+ end
36
+ end
37
+
38
+ def initialize(package, logger: ::Logger.new(nil))
39
+ @package = package
40
+ @logger = logger
41
+
42
+ @procedure = @package
43
+ .find(SERVICE_NAME, by: :ident, as: 'Protod::Proto::Service')
44
+ .find(Protod::Proto::Procedure.new(ident: PROCEDURE_NAME), by: :ident)
45
+
46
+ @request_receiver_fields = request_proto_message.find(ONEOF_NAME, by: :ident, as: 'Protod::Proto::Oneof')
47
+ @response_receiver_fields = response_proto_message.find(ONEOF_NAME, by: :ident, as: 'Protod::Proto::Oneof')
48
+
49
+ @loaded_objects = {}
50
+ end
51
+
52
+ def request_proto_message
53
+ @package.find(@procedure.request_ident, by: :ident, as: 'Protod::Proto::Message')
54
+ end
55
+
56
+ def response_proto_message
57
+ @package.find(@procedure.response_ident, by: :ident, as: 'Protod::Proto::Message')
58
+ end
59
+
60
+ def register_receiver(request_field, response_field)
61
+ @request_receiver_fields.find_or_push(request_field, by: :ident, into: :fields)
62
+ @response_receiver_fields.find_or_push(response_field, by: :ident, into: :fields)
63
+ end
64
+
65
+ def handle(req_pb)
66
+ receiver_name = req_pb.public_send(ONEOF_NAME)
67
+
68
+ return unless receiver_name
69
+
70
+ req_packet = @request_receiver_fields.find(receiver_name, by: :ident, as: 'Protod::Proto::Field').then do |f|
71
+ raise InvalidArgument, "Not found acceptable receiver : #{receiver_name}" unless f
72
+
73
+ f.interpreter.to_rb_from(req_pb.public_send(receiver_name))
74
+ end
75
+
76
+ receiver = if req_packet.receiver_id
77
+ @loaded_objects[req_packet.receiver_id.to_s] or raise InvalidArgument, "Invalid object_id in request for #{receiver_name}"
78
+ else
79
+ req_packet.receiver
80
+ end
81
+
82
+ raise InvalidArgument, "Not found receiver in request for #{receiver_name}" unless receiver
83
+ raise InvalidArgument, "Not found #{req_packet.procedure} in receiver public methods for #{receiver_name}" unless receiver.respond_to?(req_packet.procedure)
84
+
85
+ @logger.debug("protod/handle call #{receiver_name}##{req_packet.procedure} : #{req_packet.args} #{req_packet.kwargs}")
86
+ rb = receiver.public_send(req_packet.procedure, *req_packet.args, **req_packet.kwargs)
87
+
88
+ memorize_object_id_of(rb)
89
+
90
+ res_pb = @response_receiver_fields.find(receiver_name, by: :ident, as: 'Protod::Proto::Field').then do |f|
91
+ f.interpreter.to_pb_from(ResponsePacket.new(procedure: req_packet.procedure, object: rb))
92
+ end
93
+
94
+ response_pb_const.new(receiver_name.to_sym => res_pb)
95
+ end
96
+
97
+ private
98
+
99
+ def response_pb_const
100
+ @response_pb_const ||= @package.find(@procedure.response_ident, by: :ident, as: 'Protod::Proto::Message').pb_const
101
+ end
102
+
103
+ def memorize_object_id_of(value)
104
+ @loaded_objects[value.object_id.to_s] = value
105
+ value.each { memorize_object_id_of(_1) } if Protod::Proto::Field.should_repeated_with(value.class)
106
+ end
107
+
108
+ class InvalidArgument < StandardError; end
109
+
110
+ RequestPacket = Struct.new(:receiver_id, :receiver, :procedure, :args, :kwargs, keyword_init: true)
111
+ ResponsePacket = Struct.new(:procedure, :object, keyword_init: true)
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,68 @@
1
+ class Protod
2
+ module Rpc
3
+ class Request
4
+ class << self
5
+ def register_for(*ruby_idents, with:, force: true, ignore: false, &body)
6
+ ruby_idents.map { Protod::RubyIdent.absolute_of(_1) }.each do |ruby_ident|
7
+ next if map.key?(ruby_ident) && ignore
8
+
9
+ raise ArgumentError, "Request already regsitered for #{ruby_ident}" if map.key?(ruby_ident) && !force
10
+
11
+ map[ruby_ident] = Class.new(with, &body).tap do |const|
12
+ const.ruby_ident = Protod::RubyIdent.build_from(ruby_ident)
13
+ end
14
+ end
15
+ end
16
+
17
+ def find_by(ruby_ident)
18
+ map[Protod::RubyIdent.absolute_of(ruby_ident)]
19
+ end
20
+
21
+ def keys
22
+ map.keys
23
+ end
24
+
25
+ def clear!
26
+ @map = nil
27
+ end
28
+
29
+ private
30
+
31
+ def map
32
+ @map ||= {}
33
+ end
34
+ end
35
+
36
+ class Base
37
+ class_attribute :ruby_ident
38
+ end
39
+
40
+ class Receiver
41
+ ONEOF_NAME = 'procedure'
42
+
43
+ class << self
44
+ def register_for(*ruby_idents, with: Base, **options, &body)
45
+ Protod::Rpc::Request.register_for(*ruby_idents, **options.merge(with: with), &body)
46
+ end
47
+ end
48
+
49
+ class Base < Protod::Rpc::Request::Base
50
+ class_attribute :procedures
51
+
52
+ def self.push_procedure(*names, singleton: false)
53
+ ruby_idents = names.map { Protod::RubyIdent.new(const_name: ruby_ident, method_name: _1, singleton: singleton) }
54
+
55
+ Protod::Rpc::Request.register_for(*ruby_idents, with: Protod::Rpc::Request::Base, force: false, ignore: true)
56
+
57
+ self.procedures ||= []
58
+ self.procedures.push(*ruby_idents)
59
+ end
60
+
61
+ def self.procedure_pushed?(name, singleton: false)
62
+ procedures&.include?(Protod::RubyIdent.new(const_name: ruby_ident, method_name: name, singleton: singleton)) || false
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,68 @@
1
+ class Protod
2
+ module Rpc
3
+ class Response
4
+ class << self
5
+ def register_for(*ruby_idents, with:, force: true, ignore: false, &body)
6
+ ruby_idents.map { Protod::RubyIdent.absolute_of(_1) }.each do |ruby_ident|
7
+ next if map.key?(ruby_ident) && ignore
8
+
9
+ raise ArgumentError, "Response already regsitered for #{ruby_ident}" if map.key?(ruby_ident) && !force
10
+
11
+ map[ruby_ident] = Class.new(with, &body).tap do |const|
12
+ const.ruby_ident = Protod::RubyIdent.build_from(ruby_ident)
13
+ end
14
+ end
15
+ end
16
+
17
+ def find_by(ruby_ident)
18
+ map[Protod::RubyIdent.absolute_of(ruby_ident)]
19
+ end
20
+
21
+ def keys
22
+ map.keys
23
+ end
24
+
25
+ def clear!
26
+ @map = nil
27
+ end
28
+
29
+ private
30
+
31
+ def map
32
+ @map ||= {}
33
+ end
34
+ end
35
+
36
+ class Base
37
+ class_attribute :ruby_ident
38
+ end
39
+
40
+ class Receiver
41
+ ONEOF_NAME = 'procedure'
42
+
43
+ class << self
44
+ def register_for(*ruby_idents, with: Base, **options, &body)
45
+ Protod::Rpc::Response.register_for(*ruby_idents, **options.merge(with: with), &body)
46
+ end
47
+ end
48
+
49
+ class Base < Protod::Rpc::Response::Base
50
+ class_attribute :procedures
51
+
52
+ def self.push_procedure(*names, singleton: false)
53
+ ruby_idents = names.map { Protod::RubyIdent.new(const_name: ruby_ident, method_name: _1, singleton: singleton) }
54
+
55
+ Protod::Rpc::Response.register_for(*ruby_idents.map(&:to_s), with: Protod::Rpc::Response::Base, force: false, ignore: true)
56
+
57
+ self.procedures ||= []
58
+ self.procedures.push(*ruby_idents)
59
+ end
60
+
61
+ def self.procedure_pushed?(name, singleton: false)
62
+ procedures&.include?(Protod::RubyIdent.new(const_name: ruby_ident, method_name: name, singleton: singleton)) || false
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,49 @@
1
+ class Protod
2
+ class RubyIdent
3
+ include ActiveModel::Model
4
+ include ActiveModel::Attributes
5
+
6
+ attribute :const_name, :string
7
+ attribute :method_name, :string
8
+ attribute :singleton, :boolean, default: false
9
+
10
+ class << self
11
+ def build_from(string)
12
+ return if string.blank?
13
+
14
+ string = string.gsub('__', '::')
15
+
16
+ const_name, method_name, singleton = case
17
+ when string.include?('.')
18
+ [*string.split('.'), true]
19
+ when string.include?('#')
20
+ [*string.split('#'), false]
21
+ else
22
+ [string, nil, false]
23
+ end
24
+
25
+ return unless const_name.safe_constantize
26
+
27
+ new(const_name: const_name, method_name: method_name, singleton: singleton)
28
+ end
29
+
30
+ def absolute_of(ruby_ident)
31
+ return if ruby_ident.blank?
32
+
33
+ "::#{ruby_ident.to_s.delete_prefix('::')}"
34
+ end
35
+ end
36
+
37
+ def const_name
38
+ self.class.absolute_of(super)
39
+ end
40
+
41
+ def ==(other)
42
+ to_s == other.to_s
43
+ end
44
+
45
+ def to_s
46
+ [const_name, method_name].compact.join(singleton ? '.' : '#')
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Protod
4
+ VERSION = "0.1.0"
5
+ end
data/lib/protod.rb ADDED
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rbs'
4
+ require 'active_support/all'
5
+ require 'active_model'
6
+
7
+ require_relative "protod/version"
8
+ require_relative 'protod/configuration'
9
+ require_relative 'protod/ruby_ident'
10
+ require_relative 'protod/proto/features'
11
+ require_relative 'protod/proto/part'
12
+ require_relative 'protod/proto/package'
13
+ require_relative 'protod/proto/service'
14
+ require_relative 'protod/proto/procedure'
15
+ require_relative 'protod/proto/message'
16
+ require_relative 'protod/proto/field'
17
+ require_relative 'protod/proto/oneof'
18
+ require_relative 'protod/proto/builder'
19
+ require_relative 'protod/interpreter'
20
+ require_relative 'protod/interpreter/builtin'
21
+ require_relative 'protod/interpreter/active_record'
22
+ require_relative 'protod/interpreter/rpc'
23
+ require_relative 'protod/rpc/request'
24
+ require_relative 'protod/rpc/response'
25
+ require_relative 'protod/rpc/handler'
26
+
27
+ class Protod
28
+ class << self
29
+ def configure(&body)
30
+ if @configuration
31
+ body.call(@configuration)
32
+ else
33
+ @configures ||= []
34
+ @configures.push(body)
35
+ end
36
+ end
37
+
38
+ def configuration
39
+ @configuration ||= Protod::Configuration.new.tap do |c|
40
+ # Interpreters will be needed by Protod::Configuration#register_interpreter_for
41
+ Protod::Interpreter::Builtin.setup!
42
+ Protod::Interpreter::ActiveRecord.setup!
43
+ Protod::Interpreter::Rpc.setup!
44
+
45
+ @configures&.each { _1.call(c) }
46
+ end
47
+ end
48
+
49
+ def clear!
50
+ @configures = nil
51
+ @configuration = nil
52
+ Protod::Proto::Package.clear!
53
+ Protod::Rpc::Request.clear!
54
+ Protod::Rpc::Response.clear!
55
+ Protod::Interpreter.clear!
56
+ end
57
+
58
+ def setup!
59
+ Protod.configuration.builders.each(&:build)
60
+ Protod::Proto::Package.roots.each(&:freeze)
61
+ Protod::Interpreter.setup_reverse_lookup!
62
+ end
63
+
64
+ concerning :GlobalUtilities do
65
+ delegate :find_or_register_package, to: Protod::Proto::Package
66
+ delegate :rbs_environment, :rbs_definition_builder, to: :configuration
67
+
68
+ def rbs_method_type_for(ruby_ident)
69
+ method_types = rbs_definition_for(ruby_ident.const_name, singleton: ruby_ident.singleton).methods[ruby_ident.method_name.to_sym]&.method_types
70
+
71
+ raise NotImplementedError, "Not found rbs for #{ruby_ident}" unless method_types
72
+
73
+ m = method_types.find { _1.block.nil? || _1.block.required.! } || method_types.first
74
+
75
+ raise ArgumentError, "Unsupported receiving block method : #{ruby_ident}" if m.block&.required
76
+
77
+ m
78
+ end
79
+
80
+ def rbs_definition_for(const_name, singleton:)
81
+ const_names = const_name.delete_prefix('::').split('::')
82
+ name = const_names.pop.to_sym
83
+ namespace = const_names.empty? ? RBS::Namespace.root : RBS::Namespace.new(path: const_names.map(&:to_sym), absolute: true)
84
+
85
+ rbs_type_name = RBS::TypeName.new(name: name, namespace: namespace)
86
+
87
+ raise NotImplementedError, "Not found rbs for #{const_name}" unless rbs_environment.class_decls.key?(rbs_type_name)
88
+
89
+ if singleton
90
+ rbs_definition_builder.build_singleton(rbs_type_name)
91
+ else
92
+ rbs_definition_builder.build_instance(rbs_type_name)
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ module Types
99
+ Binary = Object.new
100
+ UnsignedInteger = Object.new
101
+ Json = Object.new
102
+ end
103
+ end
data/sig/protod.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Protod
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end