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