twirp 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
@@ -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
@@ -0,0 +1,3 @@
1
+ # Ruby Twirp
2
+
3
+ Twirp services and clients in Ruby.
@@ -0,0 +1,2 @@
1
+ require_relative 'twirp/version'
2
+ require_relative 'twirp/error'
@@ -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
@@ -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,3 @@
1
+ module Twirp
2
+ VERSION = "0.0.1"
3
+ 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
+ }
@@ -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
+
@@ -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