twirp 0.0.1
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.
- checksums.yaml +7 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +20 -0
- data/README.md +3 -0
- data/lib/twirp.rb +2 -0
- data/lib/twirp/error.rb +86 -0
- data/lib/twirp/server.rb +66 -0
- data/lib/twirp/version.rb +3 -0
- data/protoc-gen-twirp_ruby/main.go +185 -0
- data/test/error_test.rb +83 -0
- data/twirp.gemspec +25 -0
- metadata +86 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: cd5d364eb9a55aa4f9f80a217d0c25a9a3b5377d
|
4
|
+
data.tar.gz: add09813fd7965e1c510b5774a30445e66859b3e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1a0e61629575ec564449543c16b6c4842541abaea0c97e944318aea5ed385941b155371cbe2926faf2fe0c50824d46d1eb44302bed983749859599b6906d9063
|
7
|
+
data.tar.gz: f4113090daee32f775653c563fdb2ba60bad0d34ccc0b2287e8636b820288411933d20e41988d43ae17035db737e3e7cdce4079ad888fd16a4fc14a4eb8ee608
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
twirp (0.0.1)
|
5
|
+
google-protobuf (>= 3.0.0)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
google-protobuf (3.5.1.2)
|
11
|
+
|
12
|
+
PLATFORMS
|
13
|
+
ruby
|
14
|
+
|
15
|
+
DEPENDENCIES
|
16
|
+
bundler (>= 1)
|
17
|
+
twirp!
|
18
|
+
|
19
|
+
BUNDLED WITH
|
20
|
+
1.14.6
|
data/README.md
ADDED
data/lib/twirp.rb
ADDED
data/lib/twirp/error.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
module Twirp
|
2
|
+
|
3
|
+
# Valid Twirp error codes and their mapping to related HTTP status.
|
4
|
+
# This can also be used to check if a code is valid (check if not nil).
|
5
|
+
ERROR_CODES_TO_HTTP_STATUS = {
|
6
|
+
canceled: 408, # RequestTimeout
|
7
|
+
invalid_argument: 400, # BadRequest
|
8
|
+
deadline_exceeded: 408, # RequestTimeout
|
9
|
+
not_found: 404, # Not Found
|
10
|
+
bad_route: 404, # Not Found
|
11
|
+
already_exists: 409, # Conflict
|
12
|
+
permission_denied: 403, # Forbidden
|
13
|
+
unauthenticated: 401, # Unauthorized
|
14
|
+
resource_exhausted: 403, # Forbidden
|
15
|
+
failed_precondition: 412, # Precondition Failed
|
16
|
+
aborted: 409, # Conflict
|
17
|
+
out_of_range: 400, # Bad Request
|
18
|
+
|
19
|
+
internal: 500, # Internal Server Error
|
20
|
+
unknown: 500, # Internal Server Error
|
21
|
+
unimplemented: 501, # Not Implemented
|
22
|
+
unavailable: 503, # Service Unavailable
|
23
|
+
data_loss: 500, # Internal Server Error
|
24
|
+
}
|
25
|
+
|
26
|
+
# List of all valid error codes in Twirp
|
27
|
+
ERROR_CODES = ERROR_CODES_TO_HTTP_STATUS.keys
|
28
|
+
|
29
|
+
# Twirp::Error represents a valid error from a Twirp service
|
30
|
+
class Error
|
31
|
+
|
32
|
+
# Initialize a Twirp::Error
|
33
|
+
# The code MUST be one of the valid ERROR_CODES Symbols (e.g. :internal, :not_found, :permission_denied ...).
|
34
|
+
# The msg is a String with the error message.
|
35
|
+
# The meta is optional error metadata, if included it MUST be a Hash with String keys and values.
|
36
|
+
def initialize(code, msg, meta=nil)
|
37
|
+
@code = validate_code(code)
|
38
|
+
@msg = msg.to_s
|
39
|
+
@meta = validate_meta(meta)
|
40
|
+
end
|
41
|
+
|
42
|
+
attr_reader :code
|
43
|
+
attr_reader :msg
|
44
|
+
def meta; @meta || {}; end
|
45
|
+
|
46
|
+
def as_json
|
47
|
+
h = {
|
48
|
+
code: @code,
|
49
|
+
msg: @msg,
|
50
|
+
}
|
51
|
+
h[:meta] = @meta if @meta
|
52
|
+
h
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_json
|
56
|
+
JSON.encode(as_json)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def validate_code(code)
|
62
|
+
if !code.is_a? Symbol
|
63
|
+
raise ArgumentError.new("Twirp::Error code must be a Symbol, but it is a #{code.class.to_s}")
|
64
|
+
end
|
65
|
+
if !ERROR_CODES_TO_HTTP_STATUS.has_key? code
|
66
|
+
raise ArgumentError.new("Twirp::Error code :#{code} is invalid. Expected one of #{ERROR_CODES.inspect}")
|
67
|
+
end
|
68
|
+
code
|
69
|
+
end
|
70
|
+
|
71
|
+
def validate_meta(meta)
|
72
|
+
return nil if !meta
|
73
|
+
if !meta.is_a? Hash
|
74
|
+
raise ArgumentError.new("Twirp::Error meta must be a Hash, but it is a #{meta.class.to_s}")
|
75
|
+
end
|
76
|
+
meta.each do |k, v|
|
77
|
+
if !k.is_a?(String) || !v.is_a?(String)
|
78
|
+
raise ArgumentError.new("Twirp::Error meta must be a Hash with String keys and values")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
meta
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
data/lib/twirp/server.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
module Twirp
|
2
|
+
class Server
|
3
|
+
@@rpcs = {}
|
4
|
+
|
5
|
+
def initialize(svc)
|
6
|
+
@svc = svc
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.rpc(name, request_class, response_class)
|
10
|
+
@@rpcs[name] = {
|
11
|
+
request_class: request_class,
|
12
|
+
response_class: response_class
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
def route_request(req)
|
17
|
+
# Parse url to get method names
|
18
|
+
method_name = req.path_info[1..-1]
|
19
|
+
|
20
|
+
# Get req/res types from @@rpcs
|
21
|
+
rpc = @@rpcs[method_name.to_sym]
|
22
|
+
request_class = rpc[:request_class]
|
23
|
+
response_class = rpc[:response_class]
|
24
|
+
|
25
|
+
case req.env["CONTENT_TYPE"]
|
26
|
+
when "application/json"
|
27
|
+
return self.serve_json(req, method_name, request_class, response_class)
|
28
|
+
when "application/protobuf"
|
29
|
+
return self.serve_proto(req, method_name, request_class, response_class)
|
30
|
+
else
|
31
|
+
return self.serve_error(Twerr.NotFound("unexpected Content-Type: #{req.env["CONTENT_TYPE"]}"))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def serve_json(req, method_name, request_class, response_class)
|
36
|
+
params = request_class.decode_json(req.body.read)
|
37
|
+
resp = @svc.send(method_name.underscore, params)
|
38
|
+
self.serve_success_json(response_class.encode_json(resp))
|
39
|
+
end
|
40
|
+
|
41
|
+
def serve_proto(req, method_name, request_class, response_class)
|
42
|
+
params = request_type.decode(req.body.read)
|
43
|
+
resp = @svc.send(method_name.underscore, params)
|
44
|
+
self.serve_success_proto(response_class.encode(resp))
|
45
|
+
end
|
46
|
+
|
47
|
+
def serve_success_proto(resp)
|
48
|
+
return ['200', {'Content-Type' => 'application/protobuf'}, [resp]]
|
49
|
+
end
|
50
|
+
|
51
|
+
def serve_success_json(resp)
|
52
|
+
return ['200', {'Content-Type' => 'application/json'}, [resp]]
|
53
|
+
end
|
54
|
+
|
55
|
+
def serve_error(twerr)
|
56
|
+
return ['500', {'Content-Type' => 'application/json'}, []]
|
57
|
+
end
|
58
|
+
|
59
|
+
def handler
|
60
|
+
return Proc.new do |env|
|
61
|
+
req = Rack::Request.new(env)
|
62
|
+
self.route_request(req)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
package main
|
2
|
+
|
3
|
+
import (
|
4
|
+
"bytes"
|
5
|
+
"flag"
|
6
|
+
"fmt"
|
7
|
+
"io"
|
8
|
+
"io/ioutil"
|
9
|
+
"log"
|
10
|
+
"os"
|
11
|
+
"path"
|
12
|
+
"strings"
|
13
|
+
"unicode"
|
14
|
+
|
15
|
+
"github.com/golang/protobuf/proto"
|
16
|
+
"github.com/golang/protobuf/protoc-gen-go/descriptor"
|
17
|
+
plugin "github.com/golang/protobuf/protoc-gen-go/plugin"
|
18
|
+
)
|
19
|
+
|
20
|
+
const Version = "v5.2.0"
|
21
|
+
|
22
|
+
func main() {
|
23
|
+
versionFlag := flag.Bool("version", false, "print version and exit")
|
24
|
+
flag.Parse()
|
25
|
+
if *versionFlag {
|
26
|
+
fmt.Println(Version)
|
27
|
+
os.Exit(0)
|
28
|
+
}
|
29
|
+
|
30
|
+
g := newGenerator()
|
31
|
+
Main(g)
|
32
|
+
}
|
33
|
+
|
34
|
+
type Generator interface {
|
35
|
+
Generate(in *plugin.CodeGeneratorRequest) *plugin.CodeGeneratorResponse
|
36
|
+
}
|
37
|
+
|
38
|
+
func Main(g Generator) {
|
39
|
+
req := readGenRequest(os.Stdin)
|
40
|
+
resp := g.Generate(req)
|
41
|
+
writeResponse(os.Stdout, resp)
|
42
|
+
}
|
43
|
+
|
44
|
+
type generator struct {
|
45
|
+
output *bytes.Buffer
|
46
|
+
}
|
47
|
+
|
48
|
+
func newGenerator() *generator {
|
49
|
+
return &generator{output: new(bytes.Buffer)}
|
50
|
+
}
|
51
|
+
|
52
|
+
func (g *generator) Generate(in *plugin.CodeGeneratorRequest) *plugin.CodeGeneratorResponse {
|
53
|
+
|
54
|
+
resp := new(plugin.CodeGeneratorResponse)
|
55
|
+
for _, name := range in.FileToGenerate {
|
56
|
+
for _, f := range in.ProtoFile {
|
57
|
+
if f.GetName() == name {
|
58
|
+
respFile := g.generateFile(f)
|
59
|
+
if respFile != nil {
|
60
|
+
resp.File = append(resp.File, respFile)
|
61
|
+
}
|
62
|
+
continue
|
63
|
+
}
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
return resp
|
68
|
+
}
|
69
|
+
|
70
|
+
func (g *generator) generateFile(file *descriptor.FileDescriptorProto) *plugin.CodeGeneratorResponse_File {
|
71
|
+
pkgName := pkgName(file)
|
72
|
+
g.P(`# Code generated by protoc-gen-twirp_ruby, DO NOT EDIT.`)
|
73
|
+
for _, service := range file.Service {
|
74
|
+
serviceName := serviceName(service)
|
75
|
+
g.P(``)
|
76
|
+
g.P(fmt.Sprintf("class %s < Twirp::Server", serviceName))
|
77
|
+
g.P(fmt.Sprintf(`PATH_PREFIX = "/twirp/%s.%s"`, pkgName, serviceName))
|
78
|
+
for _, method := range service.GetMethod() {
|
79
|
+
methName := methodName(method)
|
80
|
+
inputName := methodInputName(method)
|
81
|
+
outputName := methodOutputName(method)
|
82
|
+
g.P(fmt.Sprintf(" rpc :%s, %s, %s", methName, inputName, outputName))
|
83
|
+
}
|
84
|
+
g.P(`end`)
|
85
|
+
}
|
86
|
+
|
87
|
+
resp := new(plugin.CodeGeneratorResponse_File)
|
88
|
+
resp.Name = proto.String(rubyFileName(file))
|
89
|
+
resp.Content = proto.String(g.output.String())
|
90
|
+
g.output.Reset()
|
91
|
+
|
92
|
+
return resp
|
93
|
+
}
|
94
|
+
|
95
|
+
func (g *generator) P(args ...string) {
|
96
|
+
for _, v := range args {
|
97
|
+
g.output.WriteString(v)
|
98
|
+
}
|
99
|
+
g.output.WriteByte('\n')
|
100
|
+
}
|
101
|
+
|
102
|
+
func rubyFileName(f *descriptor.FileDescriptorProto) string {
|
103
|
+
name := *f.Name
|
104
|
+
if ext := path.Ext(name); ext == ".proto" || ext == ".protodevel" {
|
105
|
+
name = name[:len(name)-len(ext)]
|
106
|
+
}
|
107
|
+
name += "_twirp.rb"
|
108
|
+
return name
|
109
|
+
}
|
110
|
+
|
111
|
+
func pkgName(file *descriptor.FileDescriptorProto) string {
|
112
|
+
return file.GetPackage()
|
113
|
+
}
|
114
|
+
|
115
|
+
func serviceName(service *descriptor.ServiceDescriptorProto) string {
|
116
|
+
return service.GetName()
|
117
|
+
}
|
118
|
+
|
119
|
+
func methodName(method *descriptor.MethodDescriptorProto) string {
|
120
|
+
return method.GetName()
|
121
|
+
}
|
122
|
+
|
123
|
+
// methodInputName returns the basename of the input type of a method in snake
|
124
|
+
// case.
|
125
|
+
func methodInputName(meth *descriptor.MethodDescriptorProto) string {
|
126
|
+
fullName := meth.GetInputType()
|
127
|
+
split := strings.Split(fullName, ".")
|
128
|
+
return split[len(split)-1]
|
129
|
+
}
|
130
|
+
|
131
|
+
// methodInputName returns the basename of the input type of a method in snake
|
132
|
+
// case.
|
133
|
+
func methodOutputName(meth *descriptor.MethodDescriptorProto) string {
|
134
|
+
fullName := meth.GetOutputType()
|
135
|
+
split := strings.Split(fullName, ".")
|
136
|
+
return split[len(split)-1]
|
137
|
+
}
|
138
|
+
|
139
|
+
func Fail(msgs ...string) {
|
140
|
+
s := strings.Join(msgs, " ")
|
141
|
+
log.Print("error:", s)
|
142
|
+
os.Exit(1)
|
143
|
+
}
|
144
|
+
|
145
|
+
// SnakeCase converts a string from CamelCase to snake_case.
|
146
|
+
func SnakeCase(s string) string {
|
147
|
+
var buf bytes.Buffer
|
148
|
+
for i, r := range s {
|
149
|
+
if unicode.IsUpper(r) && i > 0 {
|
150
|
+
fmt.Fprintf(&buf, "_")
|
151
|
+
}
|
152
|
+
r = unicode.ToLower(r)
|
153
|
+
fmt.Fprintf(&buf, "%c", r)
|
154
|
+
}
|
155
|
+
return buf.String()
|
156
|
+
}
|
157
|
+
|
158
|
+
func readGenRequest(r io.Reader) *plugin.CodeGeneratorRequest {
|
159
|
+
data, err := ioutil.ReadAll(os.Stdin)
|
160
|
+
if err != nil {
|
161
|
+
Fail(err.Error(), "reading input")
|
162
|
+
}
|
163
|
+
|
164
|
+
req := new(plugin.CodeGeneratorRequest)
|
165
|
+
if err = proto.Unmarshal(data, req); err != nil {
|
166
|
+
Fail(err.Error(), "parsing input proto")
|
167
|
+
}
|
168
|
+
|
169
|
+
if len(req.FileToGenerate) == 0 {
|
170
|
+
Fail("no files to generate")
|
171
|
+
}
|
172
|
+
|
173
|
+
return req
|
174
|
+
}
|
175
|
+
|
176
|
+
func writeResponse(w io.Writer, resp *plugin.CodeGeneratorResponse) {
|
177
|
+
data, err := proto.Marshal(resp)
|
178
|
+
if err != nil {
|
179
|
+
Fail(err.Error(), "marshaling response")
|
180
|
+
}
|
181
|
+
_, err = w.Write(data)
|
182
|
+
if err != nil {
|
183
|
+
Fail(err.Error(), "writing response")
|
184
|
+
}
|
185
|
+
}
|
data/test/error_test.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
|
3
|
+
require_relative '../lib/twirp/error'
|
4
|
+
|
5
|
+
class TestErrorCodes < Minitest::Test
|
6
|
+
|
7
|
+
def test_error_codes
|
8
|
+
assert_equal 17, Twirp::ERROR_CODES.size
|
9
|
+
|
10
|
+
# all codes should be symbols
|
11
|
+
Twirp::ERROR_CODES.each do |code|
|
12
|
+
assert_instance_of Symbol, code
|
13
|
+
end
|
14
|
+
|
15
|
+
# check some codes
|
16
|
+
assert_includes Twirp::ERROR_CODES, :internal
|
17
|
+
assert_includes Twirp::ERROR_CODES, :not_found
|
18
|
+
assert_includes Twirp::ERROR_CODES, :invalid_argument
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_codes_to_http_status
|
22
|
+
assert_equal 17, Twirp::ERROR_CODES_TO_HTTP_STATUS.size
|
23
|
+
|
24
|
+
assert_equal 404, Twirp::ERROR_CODES_TO_HTTP_STATUS[:not_found]
|
25
|
+
assert_equal 500, Twirp::ERROR_CODES_TO_HTTP_STATUS[:internal]
|
26
|
+
|
27
|
+
# nil for invalid_codes
|
28
|
+
assert_nil Twirp::ERROR_CODES_TO_HTTP_STATUS[:invalid_fdsafda]
|
29
|
+
assert_nil Twirp::ERROR_CODES_TO_HTTP_STATUS[500]
|
30
|
+
assert_nil Twirp::ERROR_CODES_TO_HTTP_STATUS[nil]
|
31
|
+
assert_nil Twirp::ERROR_CODES_TO_HTTP_STATUS["not_found"] # string checks not supported, please use symbols
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class TestTwirpError < Minitest::Test
|
36
|
+
|
37
|
+
def test_new_with_valid_code_and_a_message
|
38
|
+
err = Twirp::Error.new(:internal, "woops")
|
39
|
+
assert_equal :internal, err.code
|
40
|
+
assert_equal "woops", err.msg
|
41
|
+
assert_equal({}, err.meta) # empty
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_new_with_valid_metadata
|
45
|
+
err = Twirp::Error.new(:internal, "woops", "meta" => "data", "for this" => "error")
|
46
|
+
assert_equal err.meta["meta"], "data"
|
47
|
+
assert_equal "error", err.meta["for this"]
|
48
|
+
assert_nil err.meta["something else"]
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_invalid_code
|
52
|
+
assert_raises ArgumentError do
|
53
|
+
Twirp::Error.new(:invalid_code, "woops")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_invalid_metadata
|
58
|
+
Twirp::Error.new(:internal, "woops") # ensure the base case doesn't error
|
59
|
+
|
60
|
+
assert_raises ArgumentError do
|
61
|
+
Twirp::Error.new(:internal, "woops", non_string: "metadata")
|
62
|
+
end
|
63
|
+
|
64
|
+
assert_raises ArgumentError do
|
65
|
+
Twirp::Error.new(:internal, "woops", "string key" => :non_string_value)
|
66
|
+
end
|
67
|
+
|
68
|
+
assert_raises ArgumentError do
|
69
|
+
Twirp::Error.new(:internal, "woops", "valid key" => "valid val", "bad_one" => 666)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_as_json
|
74
|
+
# returns a hash with attributes
|
75
|
+
err = Twirp::Error.new(:internal, "err msg", "key" => "val")
|
76
|
+
assert_equal({code: :internal, msg: "err msg", meta: {"key" => "val"}}, err.as_json)
|
77
|
+
|
78
|
+
# skips meta if not included
|
79
|
+
err = Twirp::Error.new(:internal, "err msg")
|
80
|
+
assert_equal({code: :internal, msg: "err msg"}, err.as_json)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
data/twirp.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
require 'twirp/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "twirp"
|
9
|
+
spec.version = Twirp::VERSION
|
10
|
+
spec.authors = ["Cyrus A. Forbes", "Mario Izquierdo"]
|
11
|
+
spec.email = ["forbescyrus@gmail.com", "tothemario@gmail.com"]
|
12
|
+
spec.summary = %q{Twirp services in Ruby.}
|
13
|
+
spec.description = %q{Twirp is a simple RPC framework with protobuf service definitions. The Twirp gem provides support for Ruby.}
|
14
|
+
spec.homepage = "https://github.com/cyrusaf/ruby-twirp"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0")
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_runtime_dependency 'google-protobuf', '>= 3.0.0'
|
23
|
+
|
24
|
+
spec.add_development_dependency 'bundler', '>= 1'
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: twirp
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Cyrus A. Forbes
|
8
|
+
- Mario Izquierdo
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2018-02-06 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: google-protobuf
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: 3.0.0
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: 3.0.0
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: bundler
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '1'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '1'
|
42
|
+
description: Twirp is a simple RPC framework with protobuf service definitions. The
|
43
|
+
Twirp gem provides support for Ruby.
|
44
|
+
email:
|
45
|
+
- forbescyrus@gmail.com
|
46
|
+
- tothemario@gmail.com
|
47
|
+
executables: []
|
48
|
+
extensions: []
|
49
|
+
extra_rdoc_files: []
|
50
|
+
files:
|
51
|
+
- Gemfile
|
52
|
+
- Gemfile.lock
|
53
|
+
- README.md
|
54
|
+
- lib/twirp.rb
|
55
|
+
- lib/twirp/error.rb
|
56
|
+
- lib/twirp/server.rb
|
57
|
+
- lib/twirp/version.rb
|
58
|
+
- protoc-gen-twirp_ruby/main.go
|
59
|
+
- test/error_test.rb
|
60
|
+
- twirp.gemspec
|
61
|
+
homepage: https://github.com/cyrusaf/ruby-twirp
|
62
|
+
licenses:
|
63
|
+
- MIT
|
64
|
+
metadata: {}
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options: []
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
requirements: []
|
80
|
+
rubyforge_project:
|
81
|
+
rubygems_version: 2.6.14
|
82
|
+
signing_key:
|
83
|
+
specification_version: 4
|
84
|
+
summary: Twirp services in Ruby.
|
85
|
+
test_files:
|
86
|
+
- test/error_test.rb
|