krpc 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,28 @@
1
+
2
+ class Module
3
+ def class_name
4
+ name.rpartition("::").last
5
+ end
6
+
7
+ def const_get_or_create(module_name, value = nil, &block)
8
+ return const_get(module_name) if const_defined?(module_name, false)
9
+ value = block.call if block_given?
10
+ const_set(module_name, value)
11
+ value
12
+ end
13
+ end
14
+
15
+ module Boolean; end
16
+ class TrueClass; include Boolean; end
17
+ class FalseClass; include Boolean; end
18
+
19
+ class String
20
+ def underscore
21
+ self.gsub(/::/, '/').
22
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
23
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
24
+ tr("-", "_").
25
+ downcase
26
+ end
27
+ end
28
+
@@ -0,0 +1,54 @@
1
+ require 'krpc/protobuf_utils'
2
+ require 'set'
3
+
4
+ module KRPC
5
+ module Decoder
6
+ OK_LENGTH = 2
7
+ OK_MESSAGE = "\x4F\x4B"
8
+ GUID_LENGTH = 16
9
+
10
+ class << self
11
+
12
+ # Given a type object, and serialized data, decode the ruby value/object
13
+ def decode(data, type, type_store)
14
+ if type.is_a?(Types::MessageType) then decode_message(data, type)
15
+ elsif type.is_a?(Types::ValueType) then decode_value(data, type)
16
+ elsif type.is_a?(Types::EnumType)
17
+ v = decode_value(data, type_store.as_type("int32"))
18
+ type.ruby_type.key(v)
19
+ elsif type.is_a?(Types::ClassType)
20
+ remote_oid = decode_value(data, type_store.as_type("uint64"))
21
+ if remote_oid != 0
22
+ type.ruby_type.new(remote_oid)
23
+ else nil end
24
+ elsif type.is_a?(Types::ListType)
25
+ msg = decode_message(data, type_store.as_type("KRPC.List"))
26
+ msg.items.map{|x| decode(x, type.value_type, type_store)}.to_a
27
+ elsif type.is_a?(Types::DictionaryType)
28
+ msg = decode_message(data, type_store.as_type("KRPC.Dictionary"))
29
+ msg.entries.map{|e| [decode(e.key, type.key_type, type_store),
30
+ decode(e.value, type.value_type, type_store)]}.to_h
31
+ elsif type.is_a?(Types::SetType)
32
+ msg = decode_message(data, type_store.as_type("KRPC.Set"))
33
+ Set.new(msg.items.map{|x| decode(x, type.value_type, type_store)}.to_a)
34
+ elsif type.is_a?(Types::TupleType)
35
+ msg = decode_message(data, type_store.as_type("KRPC.Tuple"))
36
+ msg.items.zip(type.value_types).map{|x,t| decode(x, t, type_store)}.to_a
37
+ else raise RuntimeError.new("Cannot decode type #{type} from data: #{data}")
38
+ end
39
+ end
40
+
41
+ def decode_value(data, type)
42
+ ProtobufUtils::Decoder.decode(data, type.protobuf_type)
43
+ end
44
+
45
+ def decode_message(data, type)
46
+ msg = type.ruby_type.new
47
+ msg.parse_from_string data.to_s
48
+ msg
49
+ end
50
+
51
+ end
52
+ end
53
+ end
54
+
@@ -0,0 +1,124 @@
1
+ require 'krpc/core_extensions'
2
+ require 'colorize'
3
+
4
+ #TODO: Pry's show-doc integration ?
5
+
6
+ module KRPC
7
+ module Doc
8
+ class << self
9
+ @@docstr_infos = {}
10
+ @@procedure_docstr_infos = {}
11
+
12
+ def docstring_for_method(method_owner, method_name)
13
+ is_static, class_cls = method_owner.class == Class ? [true, method_owner] : [false, method_owner.class]
14
+ service_module_name, class_name = ruby_class_to_pb_module_class_pair(class_cls)
15
+ key = [service_module_name, is_static, class_name, method_name.to_s].hash
16
+ if (@@docstr_infos.has_key? key)
17
+ construct_docstring(*@@docstr_infos[key], true)
18
+ else
19
+ "No docstring for #{class_cls.name}#{calc_separator(is_static)}#{method_name.to_s} method" +
20
+ (method_owner.respond_to?(method_name) ? "" : "\nThere is no such method -- maybe a typo")
21
+ end
22
+ end
23
+
24
+ def docstring_for_procedure(service_name, procedure_name)
25
+ key = [service_name, procedure_name].hash
26
+ if (@@procedure_docstr_infos.has_key? key)
27
+ construct_docstring(service_name, '.', procedure_name, *@@procedure_docstr_infos[key][3..-1], false)
28
+ else
29
+ "No docstring for #{service_name}.#{procedure_name} procedure"
30
+ end
31
+ end
32
+
33
+ def add_docstring_info(is_static, cls, method_name, service_name="", procedure_name="", param_names=[], param_types=[], param_default=[], return_type: nil)
34
+ service_module_name = service_name == cls.class_name ? Services.class_name : service_name
35
+ key0 = [service_name, procedure_name].hash
36
+ key1 = [service_module_name, false, cls.class_name, method_name].hash
37
+ val = [cls.krpc_name, calc_separator(is_static), method_name, param_names, param_types, param_default, return_type]
38
+ @@docstr_infos[key1] = @@procedure_docstr_infos[key0] = val
39
+ if is_static
40
+ key2 = [service_module_name, true, cls.class_name, method_name].hash
41
+ @@docstr_infos[key2] = val
42
+ end
43
+ end
44
+
45
+ def add_special_docstring_info(key, value)
46
+ @@docstr_infos[key] = value
47
+ end
48
+
49
+ private #----------------------------------
50
+
51
+ def ruby_class_to_pb_module_class_pair(ruby_class)
52
+ return ["", Client.class_name] if ruby_class == Client
53
+ rest, _, pb_class_name = ruby_class.name.rpartition("::")
54
+ _, _, pb_service_name = rest.rpartition("::")
55
+ [pb_service_name, pb_class_name]
56
+ end
57
+
58
+ def calc_separator(is_static)
59
+ is_static ? '.' : '#'
60
+ end
61
+
62
+ def construct_docstring(namespace, separator, name, param_names, param_types, param_default, return_type, is_hide_this_param)
63
+ def type2str(type)
64
+ return "nil" if type.nil?
65
+ return type.class_name if type.class == Class
66
+ rt = type.ruby_type
67
+ if type.is_a?(Types::EnumType) then "Enum" + rt.keys.to_s
68
+ elsif type.is_a?(Types::ListType) ||
69
+ type.is_a?(Types::SetType)
70
+ "#{rt.class_name}[#{type2str(type.value_type)}]"
71
+ elsif type.is_a?(Types::DictionaryType)
72
+ %Q{#{rt.class_name}[#{type2str(type.key_type)} => #{type2str(type.value_type)}]}
73
+ elsif type.is_a?(Types::TupleType)
74
+ %Q{#{rt.class_name}[#{type.value_types.map{|x| type2str(x)}.join(", ")}]}
75
+ else rt.class_name end
76
+ end
77
+ param_infos = param_names.zip(param_types.map{|x| type2str(x)}, param_default)
78
+ param_infos.shift if is_hide_this_param && param_names[0] == "this"
79
+ if param_infos.empty?
80
+ params = ""
81
+ else
82
+ params = "\n" + param_infos.map do |pi|
83
+ "\t#{pi[0].light_green} :#{pi[1]}" + (pi[2].nil? ? "" : " = #{pi[2]}".magenta)
84
+ end.join(",\n") + "\n"
85
+ end
86
+ "#{namespace.cyan}#{separator.cyan}#{name.bold}(#{params}) :#{type2str(return_type).light_red}"
87
+ end
88
+
89
+ end
90
+
91
+ module SuffixMethods
92
+ DOCSTRING_SUFFIX = "_doc"
93
+ DOCSTRING_SUFFIX_REGEX = /^(.+)(?:#{DOCSTRING_SUFFIX}(=)?)$/
94
+
95
+ def self.included(base)
96
+ base.extend self
97
+ class << base
98
+ def krpc_name
99
+ class_name
100
+ end
101
+ end
102
+ end
103
+
104
+ def method_missing(method, *args, &block)
105
+ if DOCSTRING_SUFFIX_REGEX =~ method.to_s
106
+ documented_method_name = $1 + $2.to_s
107
+ if respond_to? documented_method_name.to_sym
108
+ return puts Doc.docstring_for_method(self,documented_method_name)
109
+ end
110
+ end
111
+ super
112
+ end
113
+
114
+ def respond_to_missing?(method, *)
115
+ if DOCSTRING_SUFFIX_REGEX =~ method.to_s
116
+ return true if respond_to? ($1 + $2.to_s).to_sym
117
+ end
118
+ super
119
+ end
120
+ end
121
+
122
+ end
123
+ end
124
+
@@ -0,0 +1,60 @@
1
+ require 'krpc/protobuf_utils'
2
+
3
+ module KRPC
4
+ module Encoder
5
+ RPC_HELLO_MESSAGE = "\x48\x45\x4C\x4C\x4F\x2D\x52\x50\x43\x00\x00\x00"
6
+ STREAM_HELLO_MESSAGE = "\x48\x45\x4C\x4C\x4F\x2D\x53\x54\x52\x45\x41\x4D"
7
+ NAME_LENGTH = 32
8
+
9
+ class << self
10
+
11
+ # Given a type object, and ruby object, encode the ruby object
12
+ def encode(obj, type, type_store)
13
+ if type.is_a?(Types::MessageType) then obj.serialize_to_string
14
+ elsif type.is_a?(Types::ValueType) then encode_value(obj, type)
15
+ elsif type.is_a?(Types::EnumType)
16
+ enum_value = type.ruby_type[obj]
17
+ encode_value(enum_value, type_store.as_type("int32"))
18
+ elsif type.is_a?(Types::ClassType)
19
+ remote_oid = if obj == nil then 0 else obj.remote_oid end
20
+ encode_value(remote_oid, type_store.as_type("uint64"))
21
+ elsif type.is_a?(Types::ListType)
22
+ msg = type_store.as_type("KRPC.List").ruby_type.new
23
+ msg.items = obj.map{|x| encode(x, type.value_type, type_store)}.to_a
24
+ msg.serialize_to_string
25
+ elsif type.is_a?(Types::DictionaryType)
26
+ entry_type = type_store.as_type("KRPC.DictionaryEntry").ruby_type
27
+ msg = type_store.as_type("KRPC.Dictionary").ruby_type.new
28
+ msg.entries = obj.map do |k,v|
29
+ entry = entry_type.new
30
+ entry.key = encode(k, type.key_type, type_store)
31
+ entry.value = encode(v, type.value_type, type_store)
32
+ entry
33
+ end
34
+ msg.serialize_to_string
35
+ elsif type.is_a?(Types::SetType)
36
+ msg = type_store.as_type("KRPC.Set").ruby_type.new
37
+ msg.items = obj.map{|x| encode(x, type.value_type, type_store)}.to_a
38
+ msg.serialize_to_string
39
+ elsif type.is_a?(Types::TupleType)
40
+ msg = type_store.as_type("KRPC.Tuple").ruby_type.new
41
+ msg.items = obj.zip(type.value_types).map{|x,t| encode(x, t, type_store)}.to_a
42
+ msg.serialize_to_string
43
+ else raise(RuntimeError, "Cannot encode object #{obj} of type #{type}")
44
+ end
45
+ end
46
+
47
+ def encode_value(value, type)
48
+ ProtobufUtils::Encoder.encode(value, type.protobuf_type)
49
+ end
50
+
51
+ def encode_request(req)
52
+ data = req.serialize_to_string
53
+ length = ProtobufUtils::Encoder.encode_nonnegative_varint(data.length)
54
+ length + data
55
+ end
56
+
57
+ end
58
+ end
59
+ end
60
+
@@ -0,0 +1,33 @@
1
+
2
+ module KRPC
3
+
4
+ class ConnectionError < Exception; end
5
+ class RPCError < Exception; end
6
+ class ValueError < Exception; end
7
+
8
+
9
+ class ArgumentErrorSig < ArgumentError
10
+ attr_reader :message_without_signature, :signature
11
+ def initialize(msg = nil, sig = nil)
12
+ @message_without_signature, @signature = msg, sig
13
+ super(signature.nil? ? msg : msg + "\n" + signature.to_s)
14
+ end
15
+ def with_signature(sig)
16
+ self.class.new(message_without_signature, sig)
17
+ end
18
+ end
19
+
20
+ class ArgumentsNumberErrorSig < ArgumentErrorSig
21
+ attr_reader :args_count, :valid_params_count_range
22
+ def initialize(args_count, valid_params_count_range, sig = nil)
23
+ @args_count, @valid_params_count_range = args_count, valid_params_count_range
24
+ valid_params_str = (valid_params_count_range.min == valid_params_count_range.max ? valid_params_count_range.min : valid_params_count_range).to_s
25
+ super("wrong number of arguments (#{args_count} for #{valid_params_str})", sig)
26
+ end
27
+ def with_signature(sig)
28
+ self.class.new(args_count, valid_params_count_range, sig)
29
+ end
30
+ end
31
+
32
+ end
33
+
@@ -0,0 +1,117 @@
1
+ require 'krpc/doc'
2
+ require 'krpc/core_extensions'
3
+
4
+ module KRPC
5
+ module Gen
6
+ AvailableToClassAndInstanceModuleName = "AvailableToClassAndInstance"
7
+
8
+ class << self
9
+ def service_gen_module(service_name)
10
+ const_get_or_create(service_name, Module.new)
11
+ end
12
+
13
+ def generate_class(service_name, class_name)
14
+ mod = service_gen_module(service_name)
15
+ mod.const_get_or_create(class_name) do
16
+ Class.new(ClassBase) do
17
+ attr_reader :service_name
18
+ @service_name = service_name
19
+ end
20
+ end
21
+ end
22
+
23
+ def generate_enum(service_name, enum_name, values)
24
+ mod = service_gen_module(service_name)
25
+ mod.const_get_or_create(enum_name) do
26
+ values.map{|ev| [ev.name.underscore.to_sym, ev.value]}.to_h
27
+ end
28
+ end
29
+
30
+ def add_rpc_method(cls, method_name, service_name, proc, client, *options)
31
+ is_static = options.include? :static
32
+ prepend_self_to_args = options.include? :prepend_self_to_args
33
+ target_module = is_static ? cls.const_get_or_create(AvailableToClassAndInstanceModuleName, Module.new) : cls
34
+ param_names, param_types, required_params_count, param_default, return_type = parse_procedure(proc, client)
35
+ method_name = method_name.underscore
36
+
37
+ target_module.instance_eval do
38
+ define_method method_name do |*args, **kwargs|
39
+ begin
40
+ args = [self] + args if prepend_self_to_args
41
+ client.rpc(service_name, proc.name, args, kwargs, param_names, param_types, required_params_count, param_default, return_type: return_type)
42
+ rescue ArgumentsNumberErrorSig => err
43
+ sig = Doc.docstring_for_method(self, method_name)
44
+ if prepend_self_to_args then raise ArgumentsNumberErrorSig.new(err.args_count - 1, (err.valid_params_count_range.min-1)..(err.valid_params_count_range.max-1), sig)
45
+ else raise err.with_signature(sig) end
46
+ rescue ArgumentErrorSig => err
47
+ raise err.with_signature(Doc.docstring_for_method(self, method_name))
48
+ end
49
+ end
50
+ end
51
+ Doc.add_docstring_info(is_static, cls, method_name, service_name, proc, param_names, param_types, param_default, return_type: return_type)
52
+ end
53
+
54
+ private #----------------------------------
55
+
56
+ def parse_procedure(proc, client)
57
+ param_names = proc.parameters.map{|p| p.name.underscore}
58
+ param_types = proc.parameters.map.with_index do |p,i|
59
+ client.type_store.get_parameter_type(i, p.type, proc.attributes)
60
+ end
61
+ param_required = proc.parameters.map{|p| not p.has_field?("default_argument")}
62
+ required_params_count = param_required.take_while{|x| x}.size
63
+ param_default = proc.parameters.zip(param_types).map do |param, type|
64
+ if param.has_field?("default_argument")
65
+ Decoder.decode(param.default_argument, type, client.type_store)
66
+ else nil
67
+ end
68
+ end
69
+ return_type = if proc.has_field?("return_type")
70
+ client.type_store.get_return_type(proc.return_type, proc.attributes)
71
+ else nil
72
+ end
73
+ [param_names, param_types, required_params_count, param_default, return_type]
74
+ end
75
+ end
76
+
77
+ module RPCMethodGenerator
78
+ def include_rpc_method(method_name, service_name, procedure_name, return_type: nil)
79
+ Gen.add_rpc_method(self.class, method_name, service_name, PB::Procedure.new(name: procedure_name, return_type: return_type), client)
80
+ end
81
+ end
82
+
83
+ module AvailableToClassAndInstanceMethodsHandler
84
+ def add_methods_available_to_class_and_instance
85
+ if const_defined? AvailableToClassAndInstanceModuleName
86
+ extend const_get(AvailableToClassAndInstanceModuleName)
87
+ include const_get(AvailableToClassAndInstanceModuleName)
88
+ end
89
+ end
90
+ end
91
+
92
+ class ClassBase
93
+ extend AvailableToClassAndInstanceMethodsHandler
94
+ include Doc::SuffixMethods
95
+
96
+ attr_reader :remote_oid
97
+
98
+ def initialize(remote_oid)
99
+ @remote_oid = remote_oid
100
+ end
101
+
102
+ alias_method :eql?, :==
103
+ def ==(other)
104
+ other.class == self.class and other.remote_oid == remote_oid
105
+ end
106
+ def hash
107
+ remote_oid.hash
108
+ end
109
+
110
+ def self.krpc_name
111
+ name[11..-1]
112
+ end
113
+ end
114
+
115
+ end
116
+ end
117
+
@@ -0,0 +1,134 @@
1
+ require 'krpc/KRPC.pb'
2
+
3
+ module KRPC
4
+ module ProtobufUtils
5
+ class << self
6
+ def create_PB_to_PB_message_class_hash(package)
7
+ protobuf_module = Kernel.const_get(package.gsub(".","::") + "::PB")
8
+ msg_classes_names = protobuf_module.constants.select{|c| protobuf_module.const_get(c,false).superclass == ::Protobuf::Message}
9
+ msg_classes_names.map do |name|
10
+ [package + "." + name.to_s, protobuf_module.const_get(name,false)]
11
+ end.to_h
12
+ end
13
+ end
14
+
15
+ module Decoder
16
+ class << self
17
+ def decode(bytes, type)
18
+ meth_name = "decode_" + type
19
+ raise RuntimeError.new("Unsupported type #{type}") unless respond_to?(meth_name)
20
+ send(meth_name, bytes)
21
+ end
22
+
23
+ # based on: https://developers.google.com/protocol-buffers/docs/encoding#varints & http://www.rubydoc.info/gems/ruby-protocol-buffers/1.0.1/ProtocolBuffers/Varint#decode-class_method & https://github.com/google/protobuf/blob/master/python/google/protobuf/internal/decoder.py#L136
24
+ def decode_varint(bytes)
25
+ decode_varint_pos(bytes)[0]
26
+ end
27
+ def decode_varint_pos(bytes)
28
+ pos = 0
29
+ result = 0
30
+ shift = 0
31
+ loop do
32
+ byte = bytes[pos].ord # <-- replace ".ord" with ".bytes[0]" if ".ord" start to cause problems again
33
+ binding.pry if byte > 130
34
+ pos += 1
35
+ result |= (byte & 0b0111_1111) << shift
36
+ return [result, pos] if (byte & 0b1000_0000) == 0
37
+ shift += 7
38
+ raise(RuntimeError, "too many bytes when decoding varint") if shift >= 64
39
+ end
40
+ end
41
+ def decode_signed_varint(bytes)
42
+ result = decode_varint(bytes)
43
+ result -= (1 << 64) if result > 0x7fffffffffffffff
44
+ result
45
+ end
46
+
47
+ alias_method :decode_int32, :decode_signed_varint
48
+ alias_method :decode_int64, :decode_signed_varint
49
+ alias_method :decode_uint32, :decode_varint
50
+ alias_method :decode_uint64, :decode_varint
51
+
52
+ # based on: https://github.com/ruby-protobuf/protobuf/search?q=pack
53
+ def decode_float(bytes)
54
+ bytes.unpack('e').first
55
+ end
56
+ def decode_double(bytes)
57
+ bytes.unpack('E').first
58
+ end
59
+ def decode_bool(bytes)
60
+ decode_varint(bytes) != 0
61
+ end
62
+ def decode_string(bytes) #TODO: String encoding should be utf-8
63
+ size, pos = decode_varint_pos(bytes)
64
+ bytes[pos..(pos+size)]
65
+ end
66
+ def decode_bytes(bytes)
67
+ size, pos = decode_varint_pos(bytes)
68
+ bytes[pos..(pos+size)]
69
+ end
70
+
71
+ end
72
+ end
73
+
74
+ module Encoder
75
+ class << self
76
+ def encode(value, type)
77
+ meth_name = "encode_" + type
78
+ raise(RuntimeError, "Unsupported type #{type}") unless respond_to?(meth_name)
79
+ send(meth_name, value)
80
+ end
81
+
82
+ # based on: http://www.rubydoc.info/gems/ruby-protocol-buffers/1.0.1/ProtocolBuffers/Varint#decode-class_method & https://github.com/google/protobuf/blob/master/python/google/protobuf/internal/encoder.py#L390
83
+ def encode_varint(value)
84
+ return [value].pack('C') if value < 0b1000_0000
85
+ result = ""
86
+ loop do
87
+ byte = value & 0b0111_1111
88
+ value >>= 7
89
+ if value == 0
90
+ return result << byte.chr
91
+ else
92
+ result << (byte | 0b1000_0000).chr
93
+ end
94
+ end
95
+ end
96
+ def encode_signed_varint(value)
97
+ value += (1 << 64) if value < 0
98
+ encode_varint(value)
99
+ end
100
+ def encode_nonnegative_varint(value)
101
+ raise(RangeError, "Value must be non-negative, got #{value}") if value < 0
102
+ encode_varint(value)
103
+ end
104
+
105
+ alias_method :encode_int32, :encode_signed_varint
106
+ alias_method :encode_int64, :encode_signed_varint
107
+ alias_method :encode_uint32, :encode_nonnegative_varint
108
+ alias_method :encode_uint64, :encode_nonnegative_varint
109
+
110
+ def encode_float(value)
111
+ [value].pack('e')
112
+ end
113
+ def encode_double(value)
114
+ [value].pack('E')
115
+ end
116
+ def encode_bool(value)
117
+ encode_varint(value ? 1 : 0)
118
+ end
119
+ def encode_string(value)
120
+ size = encode_varint(value.bytesize)
121
+ size + value.bytes.map(&:chr).join.b
122
+ end
123
+ def encode_bytes(value)
124
+ size = encode_varint(value.size)
125
+ size + value.map(&:chr).join.b
126
+ end
127
+
128
+ end
129
+ end
130
+
131
+ end
132
+ end
133
+
134
+