BASH - REST-API - JSON封装(中)

  |   0 评论   |   803 浏览

BASH语言本身作为脚本语言, 功能比较强大.但是语法又特别弱.

有时候我们需要对程序的接口函数进行标准封装. 比如一般的 shell函数的返回值只有 exit codestdout两种. 但是我们想要得到一般意义的函数返回 ApiResponse的时候. 这个时候怎么办呢. 这篇文章就是一个基本的技巧.

REST风格JSON API返回封装

如果说,我们想要一个标准JSON结构的返回, 这样我们就可以像JAVA API一样把return code和msg以及data一起从标准返回(对于shell来说就只有标准的输出了). 比如:

返回数据的标准Json:

{
  "ret": true,
  "errmsg": "this is error msg",
  "errcode": 0,
  "data": {
    "A": {
      "a": "b"
    },
    "B": "2y",
    "c": true,
    "d": 12346
  }
}

在使用上,我们想要的效果是:

API_RESPONSE_RETURN '{"ret":true,"errmsg":"this is errorMsg","errorcode": 0}'
API_RESPONSE_RETURN true "$data"
API_RESPONSE_RETURN false "$msg"

这个时候,我们不再依赖 exit code来判定函数的返回状态了. 所有的数据都变成一个标准的JSON输出. 这样调用方就可以很方便的获取函数的返回状态与数据了. 具体怎么实现, 下面我们接着讲. 要实现对JSON数据的操作,那就要用到一个特别强大的命令. jq

jq 基本用法

这一小节是对jq命令的基本介绍, 如果已经熟悉的可以直接跳过,直接阅读下一章节.

安装

如果是mac环境,那安装非常简单. 使用brew命令直接安装.

brew install jq

格式化

如果你输入了一个json, 此时我想要它格式化为一个可读的状态,可以使用:

echo  '{"key":"value"}' | jq .

此时可以得到一个格式化好的输出.

image.png

读取字段

比如我们有如下的json , 如果我们要取这个数据的 name 字段,怎么办呢.

{
  "key": "value",
  "data":{
      "name": "jim",
      "age": 15
  }
}

我们可以使用 .data.name 这样的过滤格式把相应的字段过滤出来

jq .data.name << EOF
{
  "key": "value",
  "data":{
      "name": "jim",
      "age": 15
  }
}
EOF

输出如下:
image.png

此时的输出是带有"的,如果要输出纯文本的内部内容.可以添加 -r 参数

jq -r .data.name << EOF
{
  "key": "value",
  "data":{
      "name": "jim",
      "age": 15
  }
}
EOF

此时的输出就是我们想要的内部本身了. 以上是基本的jq操作读取json的基本用法. 我们这里的目的是要先生成JSON返回. 因此我们还要进一步的学习json的生成相关的操作方法.

基于JSON 的 模板字段填充.

我们的JSON API的返回格式如下:

{
    "ret":true,
    "errcode": 0,
    "errmsg": "no error",
    "data": {
        "key":"value",
        "age": 10 ,
        "opResult": true
    }
}

因此,我们的返回里面是有一些固定的模板字段. ret,errcode,errmsg , 这几个字段是必须且固定的. 因此我们在生成的时候是希望能够直接提供这几个值.然后能够直接拼接出我们要的JSON对象. 这个就是最基本的模板填充.

此基本用法的语法可以参考访问: jq-creating-updating-json
o --null-input/-n:
Don't read any input at all! Instead, the filter is run once using null as the input. This is useful when using jq as a simple calculator or to construct JSON data from scratch.

此时我们先把json写为模板string. 比如:

jq -n --arg ret true --arg errmsg "no error" --arg errcode 0 '{"ret":$ret,"errcode":$errcode,"errmsg": $errmsg}'

# output
{
  "ret": "true",
  "errcode": "0",
  "errmsg": "no error"
}

此时, 基本完成了我们想要的模板渲染的目标. 但是这个结果看起来有一点怪. 就是 ret应该是一个bool值, 理应该是可以直接写为true,而不需要写为"true"的. 上面的errcode也是这个问题. 里面是一个int. 理应是也是可以直接返回0的.而不用引号包起来.

上面的json格式.在java的json反序列化工具是兼容的. 换言之这个结果给java进行读取. java是可以正常识别的.

此时我们可以使用另外一个argjson参数, 这个参数可以把一个预定义的JSON对象. 此时如果value是一个json对象(如:'{}' 或者是 null ).或者是原始值.(1 , 2 , true 这些 )

○   --arg name value:

           This option passes a value to the jq program as a predefined variable. If you run jq with --arg foo bar, then $foo is available in  the
           program and has the value "bar". Note that value will be treated as a string, so --arg foo 123 will bind $foo to "123".

           Named arguments are also available to the jq program as $ARGS.named.
○   --argjson name JSON-text:

           This  option passes a JSON-encoded value to the jq program as a predefined variable. If you run jq with --argjson foo 123, then $foo is
           available in the program and has the value 123.

我们把脚本改成如下:

jq -n --argjson ret true --arg errmsg "no error" --argjson  errcode 0 '{"ret":$ret,"errcode":$errcode,"errmsg": $errmsg}'
# output 
{
  "ret": true,
  "errcode": 0,
  "errmsg": "no error"
}

此时我们已经有了基本的JSON API标准数据模板了. 对于DATA的构建,还需要我们进一步的研究.

基于JSON 的 data 字段填充

对于data字段. 我们一般认为其是一个map或者是一个字典. 这个时候就没有办法用已经存在的json模板进行填充. 我们想实现类型于java的map的接口.

Map<String,Object> obj = new HashMap<>();
obj.put("key","value");

Map<String,Object> data = new HashMap<>();
data.put("name","jim");
data.put("age": 1000);
data.put("embeddedObj": obj);

String dataJson = JSON.toJsonString(data);

这个又该怎么实现呢? 我们需要用到 jq命令的 json生成与合并方法. jq的命令中有一个操作符号: += , 比如我原来有数据.

echo '{"age": 100}' | jq '. += {"key": "value"}'
# output
{
  "age": 100,
  "key": "value"
}

在上面的命令中, 原来有一个JSON: {"age":100} ,现在给其动态的添加了一个{"key":"value"} , 上面的keyvalue 是固定值, 我们需要使用动态参数才能实现动态填充. 由于jq的单引号的command 无法填充shell的变量. 同时这里也无法使用到jq的 模板填充功能. 因此需要做一些变化.:

if [[ "${val}" =~ ^[0-9]+(.[0-9]+){0,1}$ ]] || [[ "$val" == "true" ]] || [[ "$val" =~ ^\{.*\}$ ]]; then
          # 场景: bool值 , 数字 , 对象 时, 直接添加
          json=$(echo "$json" | jq '. +=  { '"${key}"' : '"${val}"'}')
else
          # value是一个string的情况下,要双引号
          json=$(echo "$json" | jq '. +=  { '"${key}"' : '"\"${val}\""'}')
fi

上面的代码会检查一个输入的value是一个Json对象还是一个string. 如果是JSON对象,就直接把string拼接到了 value的位置. 因为我们知道一个JSON对象是以 [0-9] | true | {} 这样的数据组成的. 因此在放到一个JSON内部的时候是不需要再转义的. 而对于一个 string. 理应首先要把它用双引号包起来,以表明其是一个完整的整体. 同时, 还需要进行一次转义. 比如 这个string "hello \" world";

注: 上面的代码并没有再进一步的处理种情况. 这个需要一个quote逻辑来进行转义.

最终实现

最后我们得到了如下的一个MAP, 或者是JSON builder. 这样就方便我们直接构建JSON:

JSON_BUILDER 函数

# JSON数据构建器. 可以像构建MAP一样构建JSON对象
# 示例:
# JSON_BUILDER key1 val1 key2 '{"key":"value"}'
#
# https://unix.stackexchange.com/questions/686785/unix-shell-quoting-issues-error-in-jq-command
# https://stackoverflow.com/questions/70617932/bash-script-to-add-a-new-key-value-pair-dynamically-in-json
JSON_BUILDER(){

  local json='{}';
  for (( cnt = 0; cnt < $#; cnt=cnt+2 )); do
        #echo "key=${*:cnt+1:1}, value=${*:cnt+2:1}"
        local key="${*:cnt+1:1}"
        local val="${*:cnt+2:1}"


        if [[ "${val}" =~ ^[0-9]+(.[0-9]+){0,1}$ ]] || [[ "$val" == "true" ]] || [[ "$val" =~ ^\{.*\}$ ]]; then
          # 场景: bool值 , 数字 , 对象 时, 直接添加
          json=$(echo "$json" | jq '. +=  { '"\"${key}\""' : '"${val}"'}')
        else
          # value是一个string的情况下,要双引号
          json=$(echo "$json" | jq '. +=  { '"\"${key}\""' : '"\"${val}\""'}')
        fi
  done
  #cat <<< $(jq '.student1 += { "Phone'"${m}"'" : '"${i}"'}' Students.json) > Students.json
  echo "${json}"
}

以下是 API_RESPONSE_RETURN

# REST_API 返回数据格式定义
# 格式示例:
# {
#     "ret": true
# }
# 参数:
# $1 - ret (true|false) 必须
# $2 - errcode          可选
# $3 - errmsg           可选
# $4 - data - jsonObject json对象 可选
# 一个参数: API_RESPONSE_RETURN true
# 两个参数: API_RESPONSE_RETURN true  jsonData
# 两个参数: API_RESPONSE_RETURN false msg
# 两个参数: API_RESPONSE_RETURN false code
API_RESPONSE_RETURN(){

  local ret=true
  local errcode=0
  local errmsg=""
  local data=null



  case $# in
    1) 
      ret=${1}
      ;;
    2)
      ret="$1"
      # echo "================"
      # echo "$2" | od -c
      # echo "================"

      # 两个参数:
      # 第二个参数可能是 code / msg / data
      # 左边的参数 $2 必须加双引号
      if [[ "$2" =~ ^[-]{0,1}[0-9]+$ ]]; then
        # errCode
          errcode=$2
      elif [[ "$2" =~ ^\{.*\}$  ]]; then
        # data
          data="$2"
      else
          errmsg="$2"
      fi
      ;;
    ?)
      ret=${1}
      errcode="$2"
      errmsg="$3"
      if [[ "$4" =~ \{.*\} ]]; then
          data="$4"
      fi
      ;;
  esac

  if [ "$ret" = false ] && [ "$errcode" = 0 ]; then
     errcode=-1
  fi

  # shellcheck disable=SC2155
  local REST_API_JSON=$(jq -n \
    --arg ret "${ret}" \
    --argjson errcode "$errcode" \
    --arg errmsg "$errmsg" \
    --argjson data "$data" \
    '
      {
          ret: $ret | test("true"),
          errcode: $errcode ,
          errmsg: $errmsg,
          data: $data
      }
      ')
  echo "$REST_API_JSON"
}

测试

最后我们测试如下的

echo "========"
API_RESPONSE_RETURN true 0 'this is error msg' "$(JSON_BUILDER A '{"a":"b"}' B 2y c true d 12346)"

echo "========"
JSON_BUILDER a 1 b 2 c true "d-_+1" hello e 0.3 data '{"a":"b"}'

echo "===2222====="
API_RESPONSE_RETURN false -1 'this is error msg'

输出:

========
{
  "ret": true,
  "errmsg": "this is error msg",
  "errcode": 0,
  "data": {
    "A": {
      "a": "b"
    },
    "B": "2y",
    "c": true,
    "d": 12346
  }
}
========
{
  "a": 1,
  "b": 2,
  "c": true,
  "d-_+1": "hello",
  "e": 0.3,
  "data": {
    "a": "b"
  }
}
===2222=====
{
  "ret": false,
  "errmsg": "this is error msg",
  "errcode": -1,
  "data": null
}

评论

发表评论


取消