diff --git a/apisix/plugins/grpc-transcode/response.lua b/apisix/plugins/grpc-transcode/response.lua index 9dd6780f049d..2487409a5e96 100644 --- a/apisix/plugins/grpc-transcode/response.lua +++ b/apisix/plugins/grpc-transcode/response.lua @@ -23,6 +23,48 @@ local string = string local ngx_decode_base64 = ngx.decode_base64 local ipairs = ipairs local pcall = pcall +local type = type +local pairs = pairs +local setmetatable = setmetatable + +pb.option "decode_default_array" +-- Protobuf repeated field label value +local PROTOBUF_REPEATED_LABEL = 3 +local repeated_label = PROTOBUF_REPEATED_LABEL + +local function fetch_proto_array_names(proto_obj) + local names = {} + if type(proto_obj) == "table" then + for k,v in pairs(proto_obj) do + if type(v) == "table" then + local sub_names = fetch_proto_array_names(v) + for sub_name,_ in pairs(sub_names) do + names[sub_name] = 1 + end + end + end + if proto_obj["label"] == repeated_label then + if proto_obj["name"] then + names[proto_obj["name"]] = 1 + end + end + end + return names +end + +local function set_default_array(tab, array_names) + if type(tab) ~= "table" then + return + end + for k, v in pairs(tab) do + if type(v) == "table" then + if array_names[k] == 1 then + setmetatable(v, core.json.array_mt) + end + set_default_array(v, array_names) + end + end +end local function handle_error_response(status_detail_type, proto) @@ -132,6 +174,9 @@ return function(ctx, proto, service, method, pb_option, show_status_in_body, sta return err_msg end + local array_names = fetch_proto_array_names(proto) + set_default_array(decoded, array_names) + local response, err = core.json.encode(decoded) if not response then err_msg = "failed to json_encode response body" diff --git a/t/plugin/grpc-transcode4.t b/t/plugin/grpc-transcode4.t new file mode 100644 index 000000000000..e92abec29175 --- /dev/null +++ b/t/plugin/grpc-transcode4.t @@ -0,0 +1,154 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +use t::APISIX 'no_plan'; +# ensure that the JSON module of Perl is installed in your test environment. +# If it is not installed, sudo cpanm JSON. +use JSON; + +no_long_string(); +no_shuffle(); +no_root_location(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: set rule +--- config + location /t { + content_by_lua_block { + local http = require "resty.http" + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/protos/1', + ngx.HTTP_PUT, + [[{ + "content" : "syntax = \"proto3\"; + package user; + service UserService { + rpc GetUserInfo(UserRequest) returns (UserResponse) {} + } + + enum Gender { + GENDER_UNSPECIFIED = 0; + GENDER_MALE = 1; + GENDER_FEMALE = 2; + } + message Job { + string items = 1; + } + message UserRequest { + string name = 1; + int32 age = 2; + } + + message UserResponse { + Gender gender = 1; + repeated string items = 2; + string message = 3; + Job job = 4; + }" + }]] + ) + + if code >= 300 then + ngx.say(body) + return + end + + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "methods": ["POST"], + "uri": "/grpctest", + "plugins": { + "grpc-transcode": { + "proto_id": "1", + "service": "user.UserService", + "method": "GetUserInfo" + } + }, + "upstream": { + "scheme": "grpc", + "type": "roundrobin", + "nodes": { + "127.0.0.1:50051": 1 + } + } + }]] + ) + + if code >= 300 then + ngx.say(body) + return + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 2: hit route +--- request +POST /grpctest +{"name":"testUser0","age":0} +--- more_headers +Content-Type: application/json +--- response_body_json +{"gender":"GENDER_MALE","message":"You are an experienced user!","items":["Senior member","Exclusive service"],"job":{"items":"Intern engineer"}} + + + +=== TEST 3: hit route +--- request +POST /grpctest +{"name":"testUser1","age":1} +--- more_headers +Content-Type: application/json +--- response_body_json +{"gender":"GENDER_FEMALE","message":"Welcome new users!","job":{"items":"junior engineer"},"items":[]} + + + +=== TEST 4: hit route +--- request +POST /grpctest +{"name":"testUser2","age":2} +--- more_headers +Content-Type: application/json +--- response_body_json +{"items":[],"message":"You are an experienced user!","job":{"items":"senior engineer"},"gender":"GENDER_UNSPECIFIED"} + + + +=== TEST 5: hit route +--- request +POST /grpctest +{"name":"testUserDefault","age":100} +--- more_headers +Content-Type: application/json +--- response_body_json +{"gender":"GENDER_UNSPECIFIED","items":[],"message":""}