grpc-rest 0.1.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 94663645d0e0541b08ccb55ad65c05c1d41391d580793f4f70f5c16925e23f34
4
- data.tar.gz: f7f3f7182934adcc2670b3a3d12a29bce8da5896284b4ee78aaa623fa4f2b163
3
+ metadata.gz: c3dc93105554c8c92a3229f693ab0c93ed5833796ef7b7f11e94cb8821f2c425
4
+ data.tar.gz: ac3ec407c674d16fb33e6bddccfa10b9e66859af3d5ecbad58d1ff2603e7c098
5
5
  SHA512:
6
- metadata.gz: ba0669e7cc0343bdc944e5ccddd0adef5f98de23173eb507c964db9cf5394987feac7f11653ce380d4e0f82fa748380646ae7d6df757f86a9fd4de322538ec72
7
- data.tar.gz: c4d29996a51dc172765a6c7489a730389d6c85d3bd3a05949cabdddbefa339b8f5166a5c2cdcb3ca263df1fc12221fe8d23873aa812cb50d26f594adec67dc89
6
+ metadata.gz: b1c05d823f083722774025bf8772d535b211b4ef01f3022baa92003a179ab2f3c1fcd6646c6b7eb5acf9ced1ff5abd3e962eea541ddfa7c0d9766bcc2649cb44
7
+ data.tar.gz: c4c40a55b73429c7716ed2e6164c546f8d1c2e53030d6b1182475b66ec430590c637c691bac7af21294a62b39782a4ecc2d332c4afa68879cef4b803a170beb1
@@ -0,0 +1,50 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches:
7
+ - main
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ test:
12
+ runs-on: [ubuntu-latest]
13
+ steps:
14
+ - name: Checkout code
15
+ uses: actions/checkout@v4
16
+ with:
17
+ fetch-depth: 0
18
+ - name: Setup Go
19
+ uses: actions/setup-go@v2
20
+ with:
21
+ go-version: '1.21'
22
+ - name: Install Protoc
23
+ uses: arduino/setup-protoc@v2
24
+ - run: git reset --hard
25
+ - run: git clean -f -d
26
+ - run: cd protoc-gen-rails && go test ./...
27
+
28
+ build_and_deploy:
29
+ needs: test
30
+ runs-on: [ubuntu-latest]
31
+ steps:
32
+ - name: Checkout code
33
+ uses: actions/checkout@v4
34
+ with:
35
+ fetch-depth: 0
36
+ - name: Setup Go
37
+ uses: actions/setup-go@v2
38
+ with:
39
+ go-version: '1.21'
40
+ - run: git reset --hard
41
+ - run: git clean -f -d
42
+ - name: Run GoReleaser
43
+ uses: goreleaser/goreleaser-action@v3
44
+ with:
45
+ distribution: goreleaser
46
+ version: latest
47
+ workdir: ./protoc-gen-rails
48
+ args: release --clean
49
+ env:
50
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
data/CHANGELOG ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## UNRELEASED
9
+
10
+ # 0.1.0 - 2024-03-01
11
+
12
+ * Initial release.
@@ -0,0 +1,77 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, sex characteristics, gender identity and expression,
9
+ level of experience, education, socio-economic status, nationality, personal
10
+ appearance, race, religion, or sexual identity and orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies within all project spaces, and it also applies when
49
+ an individual is representing the project or its community in public spaces.
50
+ Examples of representing a project or community include using an official
51
+ project e-mail address, posting via an official social media account, or acting
52
+ as an appointed representative at an online or offline event. Representation of
53
+ a project may be further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72
+
73
+ [homepage]: https://www.contributor-covenant.org
74
+
75
+ For answers to common questions about this code of conduct, see
76
+ https://www.contributor-covenant.org/faq
77
+
data/README.md CHANGED
@@ -23,7 +23,7 @@ The protobuf generator uses the same annotations as [grpc-gateway](https://githu
23
23
 
24
24
  ## Installation
25
25
 
26
- First, download `protoc-gen-rails` from the releases page on the right. Ensure it's somewhere in your PATH.
26
+ First, download `protoc-gen-rails` from the releases page on the right and unzip it. Ensure the binary is somewhere in your PATH.
27
27
 
28
28
  Then, add the following to your `Gemfile`:
29
29
 
@@ -97,8 +97,15 @@ and the generated controller will look like this:
97
97
  require 'services/example/example_services_pb'
98
98
  class ExampleServiceController < ActionController::Base
99
99
  protect_from_forgery with: :null_session
100
+
101
+ METHOD_PARAM_MAP = {
100
102
 
101
- rescue_from Google::Protobuf::TypeError do |e|
103
+ "example" => [
104
+ {name: "name", val: nil, split_name:["name"]},
105
+ ],
106
+ }.freeze
107
+
108
+ rescue_from StandardError do |e|
102
109
  render json: GrpcRest.error_msg(e)
103
110
  end
104
111
 
@@ -119,6 +126,36 @@ Rails.application.routes.draw do
119
126
  end
120
127
  ```
121
128
 
129
+ ### Hooking up Callbacks
130
+
131
+ If you're using [gruf](https://github.com/bigcommerce/gruf), as long as your Gruf controllers are loaded on application load, you don't have to do anything else - grpc-rest will automatically hook the callbacks up. If you're not, you have to tell GrpcRest about your server. An example might look like this:
132
+
133
+ ```ruby
134
+ # grpc_setup.rb, a shared library file somewhere in the app
135
+
136
+ def grpc_server
137
+ s = GRPC::RpcServer.new
138
+ s.handle(MyImpl.new) # handler inheriting from your service class - see https://grpc.io/docs/languages/ruby/basics/
139
+ s
140
+ end
141
+
142
+ # startup script for gRPC
143
+
144
+ require "grpc_setup"
145
+ server = grpc_server
146
+ server.run_till_terminated_or_interrupted([1, 'int', 'SIGQUIT'])
147
+
148
+ # Rails initializer
149
+ require "grpc_setup"
150
+ server = grpc_server
151
+ GrpcRest.register_server(server)
152
+ ```
153
+
154
+ ## To Do
155
+
156
+ * Support repeated fields via comma-separation (matches grpc-gateway, but is it really useful?)
157
+ * Install via homebrew and/or have the binary in the gem itself
158
+
122
159
  ## Contributing
123
160
 
124
161
  Bug reports and pull requests are welcome on GitHub at https://github.com/flipp-oss/grpc-rest.
@@ -1,3 +1,3 @@
1
1
  module GrpcRest
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.2"
3
3
  end
data/lib/grpc_rest.rb CHANGED
@@ -1,6 +1,14 @@
1
1
  module GrpcRest
2
2
  class << self
3
3
 
4
+ def register_server(server)
5
+ @server = server
6
+ end
7
+
8
+ def underscore(s)
9
+ GRPC::GenericService.underscore(s)
10
+ end
11
+
4
12
  # Gets a sub record from a proto. If it doesn't exist, initialize it and set it to the proto,
5
13
  # then return it.
6
14
  def sub_field(proto, name)
@@ -8,6 +16,8 @@ module GrpcRest
8
16
  return existing if existing
9
17
 
10
18
  descriptor = proto.class.descriptor.to_a.find { |a| a.name == name }
19
+ return nil if descriptor.nil?
20
+
11
21
  klass = descriptor.submsg_name.split('.').map(&:camelize).join('::').constantize
12
22
  sub_record = klass.new
13
23
  proto.public_send(:"#{name}=", sub_record)
@@ -18,11 +28,12 @@ module GrpcRest
18
28
  tokens = path.split('.')
19
29
  tokens[0...-1].each do |path_seg|
20
30
  proto = sub_field(proto, path_seg)
31
+ return if proto.nil?
21
32
  end
22
- proto.public_send(:"#{tokens.last}=", value)
33
+ proto.public_send(:"#{tokens.last}=", value) if proto.respond_to?(:"#{tokens.last}=")
23
34
  end
24
35
 
25
- def assign_params(request, param_string, body_string, params)
36
+ def assign_params(request, param_hash, body_string, params)
26
37
  parameters = params.to_h.deep_dup
27
38
  # each instance of {variable} means that we set the corresponding param variable into the
28
39
  # Protobuf request
@@ -30,11 +41,13 @@ module GrpcRest
30
41
  # to set it - e.g. {subrecord.foo} means we need to set the value of `request.subrecord.foo` to `params[:foo].`
31
42
  # We can also do simple wildcard replacement if there's a * - for example, {name=something-*}
32
43
  # means we should set `request.name` to "something-#{params[:name]}".
33
- param_string.scan(/\{(.*?)}/).each do |match|
34
- name, val = match[0].split('=')
35
- val = '*' if val.blank?
36
- name_tokens = name.split('.')
37
- assign_value(request, name, val.gsub('*', parameters.delete(name_tokens.last)))
44
+ param_hash.each do |entry|
45
+ name_tokens = entry[:split_name]
46
+ value_to_use = parameters.delete(name_tokens.last)
47
+ if entry[:val]
48
+ value_to_use = value_to_use.gsub('*', entry[:val])
49
+ end
50
+ assign_value(request, entry[:name], value_to_use)
38
51
  end
39
52
  if body_string.present? && body_string != '*'
40
53
  # we need to "splat" the body parameters into the given sub-record rather than into the top-level.
@@ -45,7 +58,7 @@ module GrpcRest
45
58
  end
46
59
 
47
60
  # assign remaining parameters
48
- parameters.except('action', 'controller').each do |k, v|
61
+ parameters.each do |k, v|
49
62
  assign_value(request, k, v)
50
63
  end
51
64
  end
@@ -62,25 +75,37 @@ module GrpcRest
62
75
  }
63
76
  end
64
77
 
78
+ def send_gruf_request(klass, service_obj, method, request)
79
+ ref = service_obj.rpc_descs[method.classify.to_sym]
80
+ handler = klass.new(
81
+ method_key: method.to_sym,
82
+ service: service_obj,
83
+ rpc_desc: ref,
84
+ active_call: nil,
85
+ message: request
86
+ )
87
+ handler.send(method.to_sym)
88
+ end
89
+
90
+ def send_grpc_request(service, method, request)
91
+ server_parts = service.split('::')
92
+ service_name = (server_parts[..-2].map { |p| underscore(p)} + [server_parts[-1]]).join('.')
93
+ route = "/#{service_name}/#{method.classify}"
94
+ handler = @server.send(:rpc_handlers)[route.to_sym]
95
+ handler.call(request)
96
+ end
97
+
65
98
  def send_request(service, method, request)
66
- service_obj = service.constantize::Service
67
- response = if defined?(Gruf)
99
+ if defined?(Gruf)
100
+ service_obj = service.constantize::Service
68
101
  klass = ::Gruf::Controllers::Base.subclasses.find do |k|
69
102
  k.bound_service == service_obj
70
103
  end
71
- ref = service_obj.rpc_descs[method.classify.to_sym]
72
- handler = klass.new(
73
- method_key: method.to_sym,
74
- service: service_obj,
75
- rpc_desc: ref,
76
- active_call: nil,
77
- message: request
78
- )
79
- handler.send(method.to_sym)
80
- else
81
- raise 'Non-gruf grpc not implemented yet!'
104
+ if klass
105
+ return send_gruf_request(klass, service_obj, method, request).to_h
106
+ end
82
107
  end
83
- response.to_h
108
+ send_grpc_request(service, method, request).to_h
84
109
  end
85
110
  end
86
111
 
@@ -0,0 +1,27 @@
1
+ project_name: protoc-gen-rails
2
+ before:
3
+ hooks:
4
+ - go mod tidy
5
+ builds:
6
+ - env:
7
+ - CGO_ENABLED=0
8
+ dir: .
9
+ goos:
10
+ - linux
11
+ - darwin
12
+ binary: protoc-gen-rails
13
+ changelog:
14
+ sort: asc
15
+ filters:
16
+ exclude:
17
+ - '^docs:'
18
+ - '^test:'
19
+
20
+ release:
21
+ github:
22
+ owner: flipp-oss
23
+ name: grpc-rest
24
+
25
+ archives:
26
+ - id: protoc-gen-rails
27
+ name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}"
@@ -5,7 +5,7 @@ go 1.22.0
5
5
  require (
6
6
  github.com/iancoleman/strcase v0.3.0
7
7
  github.com/stretchr/testify v1.8.4
8
- google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe
8
+ google.golang.org/genproto/googleapis/api v0.0.0-20240228224816-df926f6c8641
9
9
  google.golang.org/protobuf v1.32.0
10
10
  )
11
11
 
@@ -24,6 +24,8 @@ google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac h1:ZL/Teoy/ZGnzyrq
24
24
  google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k=
25
25
  google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe h1:0poefMBYvYbs7g5UkjS6HcxBPaTRAmznle9jnxYoAI8=
26
26
  google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA=
27
+ google.golang.org/genproto/googleapis/api v0.0.0-20240228224816-df926f6c8641 h1:SO1wX9btGFrwj9EzH3ocqfwiPVOxfv4ggAJajzlHA5s=
28
+ google.golang.org/genproto/googleapis/api v0.0.0-20240228224816-df926f6c8641/go.mod h1:wLupoVsUfYPgOMwjzhYFbaVklw/INms+dqTp0tc1fv8=
27
29
  google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
28
30
  google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
29
31
  gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -0,0 +1,31 @@
1
+ // Copyright 2015 Google LLC
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ syntax = "proto3";
16
+
17
+ package google.api;
18
+
19
+ import "google/api/http.proto";
20
+ import "google/protobuf/descriptor.proto";
21
+
22
+ option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
23
+ option java_multiple_files = true;
24
+ option java_outer_classname = "AnnotationsProto";
25
+ option java_package = "com.google.api";
26
+ option objc_class_prefix = "GAPI";
27
+
28
+ extend google.protobuf.MethodOptions {
29
+ // See `HttpRule`.
30
+ HttpRule http = 72295728;
31
+ }
@@ -0,0 +1,275 @@
1
+ // Copyright 2016 Google Inc.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ syntax = "proto3";
15
+ package google.api;
16
+ option cc_enable_arenas = true;
17
+ option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
18
+ option java_multiple_files = true;
19
+ option java_outer_classname = "HttpProto";
20
+ option java_package = "com.google.api";
21
+ option objc_class_prefix = "GAPI";
22
+ // Defines the HTTP configuration for a service. It contains a list of
23
+ // [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method
24
+ // to one or more HTTP REST API methods.
25
+ message Http {
26
+ // A list of HTTP configuration rules that apply to individual API methods.
27
+ //
28
+ // **NOTE:** All service configuration rules follow "last one wins" order.
29
+ repeated HttpRule rules = 1;
30
+ }
31
+ // `HttpRule` defines the mapping of an RPC method to one or more HTTP
32
+ // REST APIs. The mapping determines what portions of the request
33
+ // message are populated from the path, query parameters, or body of
34
+ // the HTTP request. The mapping is typically specified as an
35
+ // `google.api.http` annotation, see "google/api/annotations.proto"
36
+ // for details.
37
+ //
38
+ // The mapping consists of a field specifying the path template and
39
+ // method kind. The path template can refer to fields in the request
40
+ // message, as in the example below which describes a REST GET
41
+ // operation on a resource collection of messages:
42
+ //
43
+ //
44
+ // service Messaging {
45
+ // rpc GetMessage(GetMessageRequest) returns (Message) {
46
+ // option (google.api.http).get = "/v1/messages/{message_id}/{sub.subfield}";
47
+ // }
48
+ // }
49
+ // message GetMessageRequest {
50
+ // message SubMessage {
51
+ // string subfield = 1;
52
+ // }
53
+ // string message_id = 1; // mapped to the URL
54
+ // SubMessage sub = 2; // `sub.subfield` is url-mapped
55
+ // }
56
+ // message Message {
57
+ // string text = 1; // content of the resource
58
+ // }
59
+ //
60
+ // The same http annotation can alternatively be expressed inside the
61
+ // `GRPC API Configuration` YAML file.
62
+ //
63
+ // http:
64
+ // rules:
65
+ // - selector: <proto_package_name>.Messaging.GetMessage
66
+ // get: /v1/messages/{message_id}/{sub.subfield}
67
+ //
68
+ // This definition enables an automatic, bidrectional mapping of HTTP
69
+ // JSON to RPC. Example:
70
+ //
71
+ // HTTP | RPC
72
+ // -----|-----
73
+ // `GET /v1/messages/123456/foo` | `GetMessage(message_id: "123456" sub: SubMessage(subfield: "foo"))`
74
+ //
75
+ // In general, not only fields but also field paths can be referenced
76
+ // from a path pattern. Fields mapped to the path pattern cannot be
77
+ // repeated and must have a primitive (non-message) type.
78
+ //
79
+ // Any fields in the request message which are not bound by the path
80
+ // pattern automatically become (optional) HTTP query
81
+ // parameters. Assume the following definition of the request message:
82
+ //
83
+ //
84
+ // message GetMessageRequest {
85
+ // message SubMessage {
86
+ // string subfield = 1;
87
+ // }
88
+ // string message_id = 1; // mapped to the URL
89
+ // int64 revision = 2; // becomes a parameter
90
+ // SubMessage sub = 3; // `sub.subfield` becomes a parameter
91
+ // }
92
+ //
93
+ //
94
+ // This enables a HTTP JSON to RPC mapping as below:
95
+ //
96
+ // HTTP | RPC
97
+ // -----|-----
98
+ // `GET /v1/messages/123456?revision=2&sub.subfield=foo` | `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: "foo"))`
99
+ //
100
+ // Note that fields which are mapped to HTTP parameters must have a
101
+ // primitive type or a repeated primitive type. Message types are not
102
+ // allowed. In the case of a repeated type, the parameter can be
103
+ // repeated in the URL, as in `...?param=A&param=B`.
104
+ //
105
+ // For HTTP method kinds which allow a request body, the `body` field
106
+ // specifies the mapping. Consider a REST update method on the
107
+ // message resource collection:
108
+ //
109
+ //
110
+ // service Messaging {
111
+ // rpc UpdateMessage(UpdateMessageRequest) returns (Message) {
112
+ // option (google.api.http) = {
113
+ // put: "/v1/messages/{message_id}"
114
+ // body: "message"
115
+ // };
116
+ // }
117
+ // }
118
+ // message UpdateMessageRequest {
119
+ // string message_id = 1; // mapped to the URL
120
+ // Message message = 2; // mapped to the body
121
+ // }
122
+ //
123
+ //
124
+ // The following HTTP JSON to RPC mapping is enabled, where the
125
+ // representation of the JSON in the request body is determined by
126
+ // protos JSON encoding:
127
+ //
128
+ // HTTP | RPC
129
+ // -----|-----
130
+ // `PUT /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: "123456" message { text: "Hi!" })`
131
+ //
132
+ // The special name `*` can be used in the body mapping to define that
133
+ // every field not bound by the path template should be mapped to the
134
+ // request body. This enables the following alternative definition of
135
+ // the update method:
136
+ //
137
+ // service Messaging {
138
+ // rpc UpdateMessage(Message) returns (Message) {
139
+ // option (google.api.http) = {
140
+ // put: "/v1/messages/{message_id}"
141
+ // body: "*"
142
+ // };
143
+ // }
144
+ // }
145
+ // message Message {
146
+ // string message_id = 1;
147
+ // string text = 2;
148
+ // }
149
+ //
150
+ //
151
+ // The following HTTP JSON to RPC mapping is enabled:
152
+ //
153
+ // HTTP | RPC
154
+ // -----|-----
155
+ // `PUT /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: "123456" text: "Hi!")`
156
+ //
157
+ // Note that when using `*` in the body mapping, it is not possible to
158
+ // have HTTP parameters, as all fields not bound by the path end in
159
+ // the body. This makes this option more rarely used in practice of
160
+ // defining REST APIs. The common usage of `*` is in custom methods
161
+ // which don't use the URL at all for transferring data.
162
+ //
163
+ // It is possible to define multiple HTTP methods for one RPC by using
164
+ // the `additional_bindings` option. Example:
165
+ //
166
+ // service Messaging {
167
+ // rpc GetMessage(GetMessageRequest) returns (Message) {
168
+ // option (google.api.http) = {
169
+ // get: "/v1/messages/{message_id}"
170
+ // additional_bindings {
171
+ // get: "/v1/users/{user_id}/messages/{message_id}"
172
+ // }
173
+ // };
174
+ // }
175
+ // }
176
+ // message GetMessageRequest {
177
+ // string message_id = 1;
178
+ // string user_id = 2;
179
+ // }
180
+ //
181
+ //
182
+ // This enables the following two alternative HTTP JSON to RPC
183
+ // mappings:
184
+ //
185
+ // HTTP | RPC
186
+ // -----|-----
187
+ // `GET /v1/messages/123456` | `GetMessage(message_id: "123456")`
188
+ // `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: "123456")`
189
+ //
190
+ // # Rules for HTTP mapping
191
+ //
192
+ // The rules for mapping HTTP path, query parameters, and body fields
193
+ // to the request message are as follows:
194
+ //
195
+ // 1. The `body` field specifies either `*` or a field path, or is
196
+ // omitted. If omitted, it assumes there is no HTTP body.
197
+ // 2. Leaf fields (recursive expansion of nested messages in the
198
+ // request) can be classified into three types:
199
+ // (a) Matched in the URL template.
200
+ // (b) Covered by body (if body is `*`, everything except (a) fields;
201
+ // else everything under the body field)
202
+ // (c) All other fields.
203
+ // 3. URL query parameters found in the HTTP request are mapped to (c) fields.
204
+ // 4. Any body sent with an HTTP request can contain only (b) fields.
205
+ //
206
+ // The syntax of the path template is as follows:
207
+ //
208
+ // Template = "/" Segments [ Verb ] ;
209
+ // Segments = Segment { "/" Segment } ;
210
+ // Segment = "*" | "**" | LITERAL | Variable ;
211
+ // Variable = "{" FieldPath [ "=" Segments ] "}" ;
212
+ // FieldPath = IDENT { "." IDENT } ;
213
+ // Verb = ":" LITERAL ;
214
+ //
215
+ // The syntax `*` matches a single path segment. It follows the semantics of
216
+ // [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String
217
+ // Expansion.
218
+ //
219
+ // The syntax `**` matches zero or more path segments. It follows the semantics
220
+ // of [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.3 Reserved
221
+ // Expansion. NOTE: it must be the last segment in the path except the Verb.
222
+ //
223
+ // The syntax `LITERAL` matches literal text in the URL path.
224
+ //
225
+ // The syntax `Variable` matches the entire path as specified by its template;
226
+ // this nested template must not contain further variables. If a variable
227
+ // matches a single path segment, its template may be omitted, e.g. `{var}`
228
+ // is equivalent to `{var=*}`.
229
+ //
230
+ // NOTE: the field paths in variables and in the `body` must not refer to
231
+ // repeated fields or map fields.
232
+ //
233
+ // Use CustomHttpPattern to specify any HTTP method that is not included in the
234
+ // `pattern` field, such as HEAD, or "*" to leave the HTTP method unspecified for
235
+ // a given URL path rule. The wild-card rule is useful for services that provide
236
+ // content to Web (HTML) clients.
237
+ message HttpRule {
238
+ // Selects methods to which this rule applies.
239
+ //
240
+ // Refer to [selector][google.api.DocumentationRule.selector] for syntax details.
241
+ string selector = 1;
242
+ // Determines the URL pattern is matched by this rules. This pattern can be
243
+ // used with any of the {get|put|post|delete|patch} methods. A custom method
244
+ // can be defined using the 'custom' field.
245
+ oneof pattern {
246
+ // Used for listing and getting information about resources.
247
+ string get = 2;
248
+ // Used for updating a resource.
249
+ string put = 3;
250
+ // Used for creating a resource.
251
+ string post = 4;
252
+ // Used for deleting a resource.
253
+ string delete = 5;
254
+ // Used for updating a resource.
255
+ string patch = 6;
256
+ // Custom pattern is used for defining custom verbs.
257
+ CustomHttpPattern custom = 8;
258
+ }
259
+ // The name of the request field whose value is mapped to the HTTP body, or
260
+ // `*` for mapping all fields not captured by the path pattern to the HTTP
261
+ // body. NOTE: the referred field must not be a repeated field and must be
262
+ // present at the top-level of request message type.
263
+ string body = 7;
264
+ // Additional HTTP bindings for the selector. Nested bindings must
265
+ // not contain an `additional_bindings` field themselves (that is,
266
+ // the nesting may only be one level deep).
267
+ repeated HttpRule additional_bindings = 11;
268
+ }
269
+ // A custom pattern is used for defining custom HTTP verb.
270
+ message CustomHttpPattern {
271
+ // The name of this custom HTTP verb.
272
+ string kind = 1;
273
+ // The path matched by this custom verb.
274
+ string path = 2;
275
+ }
@@ -27,6 +27,7 @@ type method struct {
27
27
  Name string
28
28
  RequestType string
29
29
  Path string
30
+ PathInfo []PathInfo
30
31
  Body string
31
32
  HttpMethod string
32
33
  }
@@ -39,18 +40,28 @@ type Route struct {
39
40
  }
40
41
 
41
42
  var controllerTemplate = `
43
+ require 'grpc_rest'
42
44
  require 'services/geo_admin/v1/test_services_pb'
43
45
  class {{.ControllerName}}Controller < ActionController::Base
44
46
  protect_from_forgery with: :null_session
45
47
 
46
- rescue_from Google::Protobuf::TypeError do |e|
48
+ rescue_from StandardError do |e|
47
49
  render json: GrpcRest.error_msg(e)
48
50
  end
51
+ METHOD_PARAM_MAP = {
52
+ {{range .Methods }}
53
+ "{{.Name}}" => [
54
+ {{range .PathInfo -}}
55
+ {name: "{{.Name}}", val: {{if .HasValPattern}}"{{.ValPattern}}"{{else}}nil{{end}}, split_name:{{.SplitName}}},
56
+ {{end -}}
57
+ ],
58
+ {{end -}}
59
+ }.freeze
49
60
  {{$fullServiceName := .FullServiceName -}}
50
61
  {{range .Methods }}
51
62
  def {{.Name}}
52
63
  grpc_request = {{.RequestType}}.new
53
- GrpcRest.assign_params(grpc_request, "{{.Path}}", "{{.Body}}", request.parameters)
64
+ GrpcRest.assign_params(grpc_request, METHOD_PARAM_MAP["{{.Name}}"], "{{.Body}}", request.parameters)
54
65
  render json: GrpcRest.send_request("{{$fullServiceName}}", "{{.Name}}", grpc_request)
55
66
  end
56
67
  {{end}}
@@ -72,12 +83,17 @@ func ProcessService(service *descriptorpb.ServiceDescriptorProto, pkg string) (F
72
83
  return FileResult{}, routes, err
73
84
  }
74
85
  httpMethod, path, err := MethodAndPath(opts.Pattern)
86
+ pathInfo, err := ParsedPath(path)
87
+ if err != nil {
88
+ return FileResult{}, routes, err
89
+ }
75
90
  controllerMethod := method{
76
91
  Name: strcase.ToSnake(m.GetName()),
77
92
  RequestType: Classify(m.GetInputType()),
78
93
  Path: path,
79
94
  HttpMethod: httpMethod,
80
95
  Body: opts.Body,
96
+ PathInfo: pathInfo,
81
97
  }
82
98
  data.Methods = append(data.Methods, controllerMethod)
83
99
  routes = append(routes, Route{
@@ -89,12 +105,12 @@ func ProcessService(service *descriptorpb.ServiceDescriptorProto, pkg string) (F
89
105
  }
90
106
  resultTemplate, err := template.New("controller").Parse(controllerTemplate)
91
107
  if err != nil {
92
- return FileResult{}, routes, err
108
+ return FileResult{}, routes, fmt.Errorf("can't parse controller template: %w", err)
93
109
  }
94
110
  var resultContent bytes.Buffer
95
111
  err = resultTemplate.Execute(&resultContent, data)
96
112
  if err != nil {
97
- return FileResult{}, routes, err
113
+ return FileResult{}, routes, fmt.Errorf("can't execute controller template: %w", err)
98
114
  }
99
115
  return FileResult{
100
116
  Content: resultContent.String(),
@@ -1,10 +1,13 @@
1
1
  package internal
2
2
 
3
3
  import (
4
- "fmt"
4
+ "encoding/json"
5
+ "fmt"
5
6
  options "google.golang.org/genproto/googleapis/api/annotations"
6
7
  "google.golang.org/protobuf/proto"
7
8
  "google.golang.org/protobuf/types/descriptorpb"
9
+ "regexp"
10
+ "strings"
8
11
  )
9
12
 
10
13
  func MethodAndPath(pattern any) (string, string, error) {
@@ -27,6 +30,40 @@ func MethodAndPath(pattern any) (string, string, error) {
27
30
  }
28
31
  }
29
32
 
33
+ type PathInfo struct {
34
+ Name string
35
+ ValPattern string
36
+ SplitName string
37
+ HasValPattern bool
38
+ }
39
+
40
+ func ParsedPath(path string) ([]PathInfo, error) {
41
+ var infos []PathInfo
42
+ re := regexp.MustCompile("\\{(.*?)}")
43
+ matches := re.FindAllString(path, -1)
44
+ for _, match := range matches {
45
+ name := match[1:len(match)-1]
46
+ val := ""
47
+ equal := strings.Index(match, "=")
48
+ if equal != -1 {
49
+ val = name[equal:]
50
+ name = name[0:equal-1]
51
+ }
52
+ splitName := strings.Split(name, ".")
53
+ jsonSplit, err := json.Marshal(splitName)
54
+ if err != nil {
55
+ return nil, fmt.Errorf("error marshalling splitName: %w", err)
56
+ }
57
+ infos = append(infos, PathInfo{
58
+ Name: name,
59
+ ValPattern: val,
60
+ SplitName: string(jsonSplit),
61
+ HasValPattern: val != "",
62
+ })
63
+ }
64
+ return infos, nil
65
+ }
66
+
30
67
  func ExtractAPIOptions(meth *descriptorpb.MethodDescriptorProto) (*options.HttpRule, error) {
31
68
  if meth.Options == nil {
32
69
  return nil, nil
@@ -12,31 +12,42 @@ import (
12
12
 
13
13
  // Overall approach taken from https://github.com/mix-php/mix/blob/master/src/grpc/protoc-gen-mix/plugin_test.go
14
14
 
15
- // When the environment variable RUN_AS_PROTOC_GEN_AVRO is set, we skip running
16
- // tests and instead act as protoc-gen-avro. This allows the test binary to
15
+ // When the environment variable RUN_AS_PROTOC_GEN_RAILS is set, we skip running
16
+ // tests and instead act as protoc-gen-rails. This allows the test binary to
17
17
  // pass itself to protoc.
18
18
  func init() {
19
- if os.Getenv("RUN_AS_PROTOC_GEN_AVRO") != "" {
19
+ if os.Getenv("RUN_AS_PROTOC_GEN_RAILS") != "" {
20
20
  main()
21
21
  os.Exit(0)
22
22
  }
23
23
  }
24
24
 
25
- func fileNames(directory string, appendDirectory bool) ([]string, error) {
26
- files, err := os.ReadDir(directory)
25
+ func fileNames(directory string, appendDirectory bool, extension string) ([]string, error) {
26
+ var files []os.FileInfo
27
+ var fullPaths []string
28
+ err := filepath.Walk(directory, func(path string, info os.FileInfo, err error) error {
29
+ if extension != "" && filepath.Ext(path) != extension {
30
+ return nil
31
+ }
32
+ if info.IsDir() {
33
+ return nil
34
+ }
35
+ files = append(files, info)
36
+ fullPaths = append(fullPaths, path)
37
+ if err != nil {
38
+ fmt.Println("ERROR:", err)
39
+ }
40
+ return nil
41
+ })
27
42
  if err != nil {
28
43
  return nil, fmt.Errorf("can't read %s directory: %w", directory, err)
29
44
  }
45
+ if appendDirectory {
46
+ return fullPaths, nil
47
+ }
30
48
  var names []string
31
- for _, file := range files {
32
- if file.IsDir() {
33
- continue
34
- }
35
- if appendDirectory {
36
- names = append(names, filepath.Base(directory) + "/" + file.Name())
37
- } else {
38
- names = append(names, file.Name())
39
- }
49
+ for _, file := range fullPaths {
50
+ names = append(names, strings.Replace(file, directory, "", 1)[1:])
40
51
  }
41
52
  return names, nil
42
53
  }
@@ -51,23 +62,23 @@ func runTest(t *testing.T, directory string, options map[string]string) {
51
62
 
52
63
  args := []string{
53
64
  "-I.",
54
- "--avro_out=" + tmpdir,
65
+ "--rails_out=" + tmpdir,
55
66
  }
56
- names, err := fileNames(workdir + "/testdata", true)
67
+ names, err := fileNames(workdir + "/testdata", false, ".proto")
57
68
  if err != nil {
58
69
  t.Fatal(fmt.Errorf("testData fileNames %w", err))
59
70
  }
60
71
  for _, name := range names {
61
- args = append(args, name)
72
+ args = append(args, fmt.Sprintf("testdata/%v", name))
62
73
  }
63
74
  for k, v := range options {
64
- args = append(args, "--avro_opt=" + k + "=" + v)
75
+ args = append(args, "--rails_opt=" + k + "=" + v)
65
76
  }
66
77
  protoc(t, args)
67
78
 
68
79
  testDir := workdir + "/testdata/" + directory
69
80
  if os.Getenv("UPDATE_SNAPSHOTS") != "" {
70
- cmd := exec.Command("/bin/sh", "-c", fmt.Sprintf("cp %v/* %v", tmpdir, testDir))
81
+ cmd := exec.Command("/bin/sh", "-c", fmt.Sprintf("cp -R %v/* %v", tmpdir, testDir))
71
82
  cmd.Run()
72
83
  } else {
73
84
  assertEqualFiles(t, testDir, tmpdir)
@@ -78,39 +89,26 @@ func Test_Base(t *testing.T) {
78
89
  runTest(t, "base", map[string]string{})
79
90
  }
80
91
 
81
- func Test_CollapseFields(t *testing.T) {
82
- runTest(t, "collapse_fields", map[string]string{"collapse_fields": "StringList"})
83
- }
84
-
85
- func Test_EmitOnly(t *testing.T) {
86
- runTest(t, "emit_only", map[string]string{"emit_only": "Widget"})
87
- }
88
-
89
- func Test_NamespaceMap(t *testing.T) {
90
- runTest(t, "namespace_map", map[string]string{"namespace_map": "testdata:mynamespace"})
91
- }
92
-
93
- func Test_PreserveNonStringMaps(t *testing.T) {
94
- runTest(t, "preserve_non_string_maps", map[string]string{"preserve_non_string_maps": "true"})
95
- }
96
-
97
92
  func assertEqualFiles(t *testing.T, original, generated string) {
98
- names, err := fileNames(original, false)
93
+ names, err := fileNames(original, false, "")
99
94
  if err != nil {
100
95
  t.Fatal(fmt.Errorf("original fileNames %w", err))
101
96
  }
102
- generatedNames, err := fileNames(generated, false)
97
+ generatedNames, err := fileNames(generated, false, "")
103
98
  if err != nil {
104
99
  t.Fatal(fmt.Errorf("generated fileNames %w", err))
105
100
  }
106
101
  assert.Equal(t, names, generatedNames)
102
+ // put back subdirectories
103
+ names, _ = fileNames(original, true, "")
104
+ generatedNames, _ = fileNames(generated, true, "")
107
105
  for i, name := range names {
108
- originalData, err := os.ReadFile(original + "/" + name)
106
+ originalData, err := os.ReadFile(name)
109
107
  if err != nil {
110
108
  t.Fatal("Can't find original file for comparison")
111
109
  }
112
110
 
113
- generatedData, err := os.ReadFile(generated + "/" + generatedNames[i])
111
+ generatedData, err := os.ReadFile(generatedNames[i])
114
112
  if err != nil {
115
113
  t.Fatal("Can't find generated file for comparison")
116
114
  }
@@ -120,9 +118,9 @@ func assertEqualFiles(t *testing.T, original, generated string) {
120
118
  }
121
119
 
122
120
  func protoc(t *testing.T, args []string) {
123
- cmd := exec.Command("protoc", "--plugin=protoc-gen-avro=" + os.Args[0])
121
+ cmd := exec.Command("protoc", "--proto_path=.", "--proto_path=./google-deps", "--plugin=protoc-gen-rails=" + os.Args[0])
124
122
  cmd.Args = append(cmd.Args, args...)
125
- cmd.Env = append(os.Environ(), "RUN_AS_PROTOC_GEN_AVRO=1")
123
+ cmd.Env = append(os.Environ(), "RUN_AS_PROTOC_GEN_RAILS=1")
126
124
  out, err := cmd.CombinedOutput()
127
125
 
128
126
  if len(out) > 0 || err != nil {
@@ -0,0 +1,43 @@
1
+
2
+ require 'grpc_rest'
3
+ require 'services/geo_admin/v1/test_services_pb'
4
+ class MyServiceController < ActionController::Base
5
+ protect_from_forgery with: :null_session
6
+
7
+ rescue_from StandardError do |e|
8
+ render json: GrpcRest.error_msg(e)
9
+ end
10
+ METHOD_PARAM_MAP = {
11
+
12
+ "test" => [
13
+ {name: "blah", val: "foobar/*", split_name:["blah"]},
14
+ {name: "repeated_string", val: nil, split_name:["repeated_string"]},
15
+ ],
16
+
17
+ "test_2" => [
18
+ ],
19
+
20
+ "test_3" => [
21
+ {name: "sub_record.sub_id", val: nil, split_name:["sub_record","sub_id"]},
22
+ ],
23
+ }.freeze
24
+
25
+ def test
26
+ grpc_request = Testdata::TestRequest.new
27
+ GrpcRest.assign_params(grpc_request, METHOD_PARAM_MAP["test"], "*", request.parameters)
28
+ render json: GrpcRest.send_request("Testdata::MyService", "test", grpc_request)
29
+ end
30
+
31
+ def test_2
32
+ grpc_request = Testdata::TestRequest.new
33
+ GrpcRest.assign_params(grpc_request, METHOD_PARAM_MAP["test_2"], "second_record", request.parameters)
34
+ render json: GrpcRest.send_request("Testdata::MyService", "test_2", grpc_request)
35
+ end
36
+
37
+ def test_3
38
+ grpc_request = Testdata::TestRequest.new
39
+ GrpcRest.assign_params(grpc_request, METHOD_PARAM_MAP["test_3"], "", request.parameters)
40
+ render json: GrpcRest.send_request("Testdata::MyService", "test_3", grpc_request)
41
+ end
42
+
43
+ end
@@ -0,0 +1,4 @@
1
+
2
+ get "/test/:blah/:repeated_string" => "my_service#test"
3
+ post "/test2" => "my_service#test_2"
4
+ post "/test3/:sub_id" => "my_service#test_3"
@@ -0,0 +1,46 @@
1
+ syntax = "proto3";
2
+
3
+ package testdata;
4
+
5
+ import "google/api/annotations.proto";
6
+
7
+ message TestRequest {
8
+ string test_id = 1;
9
+ string foobar = 2;
10
+ repeated string repeated_string = 3;
11
+ SubRecord sub_record = 4;
12
+ SubRecord second_record = 5;
13
+ }
14
+
15
+ message SubRecord {
16
+ string sub_id = 1;
17
+ string another_id = 2;
18
+ }
19
+
20
+ message TestResponse {
21
+ int32 some_int = 1;
22
+ string full_response = 2;
23
+ }
24
+
25
+ service MyService {
26
+ rpc Test(TestRequest) returns (TestResponse) {
27
+ option (google.api.http) = {
28
+ get: "/test/{blah=foobar/*}/{repeated_string}"
29
+ body: "*"
30
+ };
31
+ }
32
+
33
+ rpc Test2(TestRequest) returns (TestResponse) {
34
+ option (google.api.http) = {
35
+ post: "/test2"
36
+ body: "second_record"
37
+ };
38
+ }
39
+
40
+ rpc Test3(TestRequest) returns (TestResponse) {
41
+ option (google.api.http) = {
42
+ post: "/test3/{sub_record.sub_id}"
43
+ };
44
+ }
45
+
46
+ }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grpc-rest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Orner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-01 00:00:00.000000000 Z
11
+ date: 2024-03-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: grpc
@@ -45,19 +45,28 @@ executables: []
45
45
  extensions: []
46
46
  extra_rdoc_files: []
47
47
  files:
48
+ - ".github/workflows/CI.yml"
48
49
  - ".gitignore"
50
+ - CHANGELOG
51
+ - CODE_OF_CONDUCT.md
49
52
  - README.md
50
53
  - grpc-rest.gemspec
51
54
  - lib/grpc_rest.rb
52
55
  - lib/grpc_rest/version.rb
56
+ - protoc-gen-rails/.goreleaser.yml
53
57
  - protoc-gen-rails/LICENSE
54
58
  - protoc-gen-rails/go.mod
55
59
  - protoc-gen-rails/go.sum
60
+ - protoc-gen-rails/google-deps/google/api/annotations.proto
61
+ - protoc-gen-rails/google-deps/google/api/http.proto
56
62
  - protoc-gen-rails/internal/output.go
57
63
  - protoc-gen-rails/internal/parse.go
58
64
  - protoc-gen-rails/internal/utils.go
59
65
  - protoc-gen-rails/main.go
60
66
  - protoc-gen-rails/main_test.go
67
+ - protoc-gen-rails/testdata/base/app/controllers/my_service_controller.rb
68
+ - protoc-gen-rails/testdata/base/config/routes/grpc.rb
69
+ - protoc-gen-rails/testdata/test_service.proto
61
70
  homepage: ''
62
71
  licenses:
63
72
  - MIT