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.
@@ -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