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 +4 -4
- data/.github/workflows/CI.yml +50 -0
- data/CHANGELOG +12 -0
- data/CODE_OF_CONDUCT.md +77 -0
- data/README.md +39 -2
- data/lib/grpc_rest/version.rb +1 -1
- data/lib/grpc_rest.rb +47 -22
- data/protoc-gen-rails/.goreleaser.yml +27 -0
- data/protoc-gen-rails/go.mod +1 -1
- data/protoc-gen-rails/go.sum +2 -0
- data/protoc-gen-rails/google-deps/google/api/annotations.proto +31 -0
- data/protoc-gen-rails/google-deps/google/api/http.proto +275 -0
- data/protoc-gen-rails/internal/output.go +20 -4
- data/protoc-gen-rails/internal/parse.go +38 -1
- data/protoc-gen-rails/main_test.go +39 -41
- data/protoc-gen-rails/testdata/base/app/controllers/my_service_controller.rb +43 -0
- data/protoc-gen-rails/testdata/base/config/routes/grpc.rb +4 -0
- data/protoc-gen-rails/testdata/test_service.proto +46 -0
- metadata +11 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c3dc93105554c8c92a3229f693ab0c93ed5833796ef7b7f11e94cb8821f2c425
|
4
|
+
data.tar.gz: ac3ec407c674d16fb33e6bddccfa10b9e66859af3d5ecbad58d1ff2603e7c098
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -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
|
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
|
-
|
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.
|
data/lib/grpc_rest/version.rb
CHANGED
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,
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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.
|
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
|
-
|
67
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
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 }}"
|
data/protoc-gen-rails/go.mod
CHANGED
@@ -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-
|
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
|
|
data/protoc-gen-rails/go.sum
CHANGED
@@ -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¶m=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
|
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, "{{.
|
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
|
-
|
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
|
16
|
-
// tests and instead act as protoc-gen-
|
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("
|
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
|
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
|
32
|
-
|
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
|
-
"--
|
65
|
+
"--rails_out=" + tmpdir,
|
55
66
|
}
|
56
|
-
names, err := fileNames(workdir + "/testdata",
|
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, "--
|
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(
|
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(
|
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-
|
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(), "
|
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,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.
|
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-
|
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
|