#!/usr/pkg/bin/qore
# -*- mode: qore; indent-tabs-mode: nil -*-

# @file rest example program for the RestClient module

/*  Copyright 2013 - 2023 Qore Technologies, s.r.o.

    Permission is hereby granted, free of charge, to any person obtaining a
    copy of this software and associated documentation files (the "Software"),
    to deal in the Software without restriction, including without limitation
    the rights to use, copy, modify, merge, publish, distribute, sublicense,
    and/or sell copies of the Software, and to permit persons to whom the
    Software is furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in
    all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
    DEALINGS IN THE SOFTWARE.
*/

%new-style
%enable-all-warnings
%require-types
%strict-args

%requires RestClient
%requires Swagger
%requires Util
%requires Mime
%requires ConnectionProvider

%try-module yaml >= 0.5
%define NoYaml
%endtry

%try-module xml >= 1.3
%define NoXml
%endtry

%try-module json >= 1.5
%define NoJson
%endtry

%requires ConnectionProvider

%exec-class RestCmd

class RestCmd {
    public {
        #! program options
        const Opts = {
            "cert":      "C,cert=s",
            "conn":      "c,connection=s",
            "fullex":    "E,full-exception",
            "sendenc":   "e,send-encoding:s",
            "header":    "H,header=s@",
            "ignoreerr": "i,ignore-errors",
            "key":       "k,key=s",
            "keypass":   "K,keypass=s",
            "lit":       "l,literal:i+",
            "list":      "L,list",
            "proxy":     "P,proxy-url=s",
            "basepath":  "p,base-path=s",
            "reformat":  "R,reformat",
            "data":      "S,serialization=s",
            "swagger":   "s,swagger=s",
            "to":        "T,timeout=i",
            "token":     "t,token=s",
            "lax":       "X,lax-parsing",
            "example":   "x,example:s",
            "tokentype": "y,token-type=s",

            "help":      "h,help",
        };

        #! recognized HTTP methods
        const Methods = {
            "GET": True,
            "PUT": True,
            "POST": True,
            "DELETE": True,
            "PATCH": True,
            "OPTIONS": True,
        };
    }

    constructor() {
        GetOpt g(Opts);
        hash<auto> opt = g.parse3(\ARGV);
        if (opt.help || (!ARGV[0] && !opt.list))
            usage();

        hash<auto> opts;

        hash<auto> swagger_opts;
        if (opt.lax) {
            swagger_opts = {"parse_flags": LM_ALL};
        }

        if (opt.swagger) {
            opts.validator = SwaggerLoader::fromFile(opt.swagger, swagger_opts);
            if (opt.basepath.val()) {
                opts.validator.setBasePath(opt.basepath);
            }
        } else if (opt.example) {
            error("option -x|--example requires -s|--swagger to provide the schema for generating the example");
        } else if (opt."list") {
            error("option -L|--list requires -s|--swagger to provide the schema for returning the list");
        }

        if (opt."list") {
            printf("URL: %s\n",  opts.validator.getTargetUrl());
            foreach hash h in (opts.validator.getPathOperationHash().pairIterator()) {
                map printf("%s /%s\n", $1.upr(), h.key), h.value;
            }
            thread_exit;
        }

        if (opt.ignoreerr) {
            opts.error_passthru = True;
        }

        *string meth = shift ARGV;
        if (!exists meth) {
            usage();
        }

        *string url = shift ARGV;
        auto body;
        if (!Methods{meth.upr()}) {
            body = url;
            url = meth;
            meth = "GET";
        } else {
            meth = meth.upr();
            body = shift ARGV;
        }

        if (!url) {
            printf("ERROR: missing URL for REST server\n");
            usage();
        }

        if (!opt.conn) {
            string orig_url = url;
            url = get_connection_url(url);
            if (url != orig_url) {
                printf("using connection: %y url: %y\n", orig_url, url);
                opt.conn = orig_url;
            }
        }

        if (opts.validator && url !~ /^[a-z]+:\/\//) {
            url = opts.validator.getTargetUrl() + (url !~ /^\// ? "/" : "") + url;
        }

        # get URI path
        hash<UrlInfo> url_info = parse_url(url);

        if (opt.example) {
            bool msg = True;
            bool request = True;
            int code = 200;
            for (int i = 0; i < opt.example.size(); ++i) {
                switch (opt.example[i]) {
                    case "q":
                        msg = False;
                        break;

                    case "r":
                        request = False;
                        if (opt.example.size() > i + 1) {
                            code = opt.example.substr(i + 1).toInt();
                        }
                        i = opt.example.size();
                        break;

                    default:
                        error("unknown character %y found in example string %y; expected \"qr\"",
                            opt.example[i]);
                }
            }

            # generate example and exit
            if (msg) {
                if (request) {
                    hash<RestExampleRequestInfo> rex = opts.validator.getExampleRequest(meth, url_info.path);
                    #printf("rex: %y\n", rex);
                    printf("%s\n", rex.request_uri);
                    map printf("%s: %s\n", $1.key, $1.value), rex.hdr.pairIterator();
                    if (rex.body) {
                        if (rex.hdr) {
                            print("\n");
                        }
                        print(rex.body + "\n");
                    }
                } else {
                    hash<RestExampleResponseInfo> res = opts.validator.getExampleResponse(meth, url_info.path, code);
                    #printf("res: %y\n", res);
                    printf("%s\n", res.response_uri);
                    map printf("%s: %s\n", $1.key, $1.value), res.hdr.pairIterator();
                    if (res.body) {
                        if (res.hdr) {
                            print("\n");
                        }
                        print(res.body + "\n");
                    }
                }
            } else {
                hash<RestQoreExampleCodeInfo> qex = request
                    ? opts.validator.getQoreExampleRequest(meth, url_info.path)
                    : opts.validator.getQoreExampleResponse(meth, url_info.path, code);
                #printf("qex: %y\n", qex);
                map printf("%s\n", $1), qex.hashdecls.iterator();
                printf("%s\n", qex.example);
            }
            thread_exit;
        }

        RestClient rest;
        string request_path;

        if (opt.conn) {
            try {
                AbstractConnection conn = get_connection(opt.conn);
                if (!(conn instanceof RestConnection)) {
                    stderr.printf("connection %y is a %y object; expecting \"RestClient\"; exiting\n", opt.conn,
                        conn.className());
                    exit(1);
                }
                rest = conn.get();
                if (!exists body) {
                    stderr.printf("ERROR: missing request path\n");
                    exit(1);
                }
                request_path = body;
                body = shift ARGV;

                # if the request path is a full URL, override the connection URL
                if (request_path =~ /^http/) {
                    rest.setURL(request_path);
                    request_path = "";
                } else if (request_path[0] == "/") {
                    # if the path is absolute, then override the connection path
                    rest.setConnectionPath();
                }
            } catch (hash<ExceptionInfo> ex) {
                if (opt.fullex) {
                    stderr.printf("%s\n", get_exception_string(ex));
                } else {
                    stderr.printf("connection %y: %s: %s: %s\n", opt.conn, get_ex_pos(ex), ex.err, ex.desc);
                }
                exit(1);
            }
        } else {
            request_path = url_info.path ?? "";

            opts.url = sprintf("%s://", url_info.protocol);
            if (url_info.username && url_info.password) {
                opts.url += sprintf("%s:%s@", url_info.username, url_info.password);
            }
            opts.url += url_info.host;
            if ((url_info.port != 80 && url_info.protocol == "http")
                || (url_info.port != 443 && url_info.protocol == "https")) {
                opts.url += sprintf(":%d", url_info.port);
            }
            opts.url += "/";

            if (opt.proxy)
                opts.proxy = opt.proxy;

            if (opt.to) {
                int to = opt.to * 1000;
                opts += {
                    "timeout": to,
                    "connect_timeout": to,
                };
            }

            if (opt.cert) {
                opts.ssl_cert_path = opt.cert;
                opts.ssl_key_path = opt.key ?? opt.cert;
                opts.ssl_key_password = opt.keypass;
            }
            rest = new RestClient(opts);
        }

        if (opt.token) {
            rest.setToken(opt.tokentype ?? "Bearer", opt.token);
        }

        foreach string h in (opt.header) {
            (*string k, *string v) = (h =~ x/([^:]+):(.+)$/); #/);
            trim k;
            trim v;
            if (!k || !v) {
                stderr.printf("invalid header %y; expecting \"key: value\" format\n", h);
                exit(1);
            }
            rest.addDefaultHeaders((k: v));
        }

        if (exists body) {
            body = parse_to_qore_value(body);
        }

        if (opt."data") {
            if (!RestClient::DataSerializationOptions{opt."data"}) {
                error("data serialization option %y is unknown; valid options: %y", opt."data",
                    RestClient::DataSerializationOptions.keys());
            }
            rest.setSerialization(opt."data");
        }

        if (opt.sendenc) {
            if (opt.sendenc === True) {
                opt.sendenc = "deflate";
            } else if (!RestClient::EncodingSupport{opt.sendenc}) {
                error("send encoding option %y is unknown; valid options: %y", opt.sendenc,
                    RestClient::EncodingSupport.keys());
            }
            rest.setSendEncoding(opt.sendenc);
        }

        hash<auto> info;
        #printf("body: %y\n", body);
        try {
            hash<auto> h;
            on_exit if (opt.lit && info) {
                showRestRequest(info, body, opt);
                showRestResponse(info, h.body, opt);
            }

            on_error rethrow $1.err, sprintf("%s (using url: %y request path: %y)", $1.desc, rest.getURL(),
                request_path ?? "");
            h = rest.doRequest(meth, request_path ?? "", body, \info);
            if (!opt.lit) {
                printf("%N\n", h.body);
            }
        } catch (hash<ExceptionInfo> ex) {
            if (opt.fullex) {
                stderr.printf("%s\n", get_exception_string(ex));
            } else {
                if (ex.err == "DESERIALIZATION-ERROR"
                    && info."response-headers"."content-type" == "text/html"
                    && info."response-body") {
                    stderr.printf("%s\n%s\n", info."response-uri", html_decode(info."response-body"));
                } else {
                    stderr.printf("%s: %s: %s", rest.getURL(), ex.err, ex.desc);
                    if (ex.arg) {
                        stderr.print(": ");
                        if (ex.arg.body.typeCode() == NT_STRING) {
                            trim ex.arg.body;
                            stderr.print(ex.arg.body);
                        } else {
                            if (ex.arg.typeCode() == NT_STRING) {
                                trim ex.arg;
                                stderr.printf("%y", ex.arg);
                            } else {
                                stderr.printf("%y", ex.arg);
                            }
                        }
                    }
                    stderr.print("\n");
                }
            }
        }
    }

    private showRestRequest(hash info, any args, *hash opt) {
        printf("> %s\n", info."request-uri");
        if (opt.lit > 1)
            printf("> %N\n", info.headers);
        if (info."request-body") {
            *string str;
            if (opt.reformat) {
                switch (info."request-serialization") {
%ifndef NoXml
                    case "xml": str = make_xmlrpc_value(args, XGF_ADD_FORMATTING); break;
                    case "rawxml": str = make_xml(args, XGF_ADD_FORMATTING); break;
%endif
%ifndef NoJson
                    case "json": str = make_json(args, JGF_ADD_FORMATTING); break;
%endif
%ifndef NoYaml
                    case "yaml": str = trim(make_yaml(args, YAML::BlockStyle)); break;
%endif
                    case "url": str = mime_get_form_urlencoded_string(args); break;
                    default: str = getBody(info); break;
                }
            }
            else
                str = getBody(info);
            printf("> %s\n", str);
        }
    }

    private showRestResponse(hash info, any rv, *hash opt) {
        if (!info."response-uri")
            return;
        printf("< %s\n", info."response-uri");
        if (opt.lit > 1)
            printf("< %N\n", info."response-headers" - "body");
        if (info."response-body") {
            if (!exists rv)
                rv = info."response-body";
            string str;
            switch (info."response-serialization") {
%ifndef NoXml
                case "xml": str = make_xmlrpc_value(rv, opt.reformat ? XGF_ADD_FORMATTING : NOTHING); break;
                case "rawxml": str = make_xml(rv, opt.reformat ? XGF_ADD_FORMATTING : NOTHING); break;
%endif
%ifndef NoJson
                case "json": str = make_json(rv, opt.reformat ? JGF_ADD_FORMATTING : NOTHING); break;
%endif
%ifndef NoYaml
                case "yaml": str = trim(make_yaml(rv, opt.reformat ? YAML::BlockStyle : NOTHING)); break;
%endif
                case "url": str = mime_get_form_urlencoded_string(rv); break;
                default:
                    if (info."response-body".typeCode() == NT_STRING)
                        str = trim(info."response-body");
                    else if (info."response-body".typeCode() == NT_BINARY)
                        str = "<" + info."response-body".toHex() + ">";
                    break;
            }
            printf("< %s\n", str);
        }
    }

    private string getBody(hash info) {
        switch (info.headers."Content-Encoding") {
            case "deflate":
            case "x-deflate":
                info."request-body" = uncompress_to_string(info."request-body");
                break;
            case "gzip":
            case "x-gzip":
                info."request-body" = gunzip_to_string(info."request-body");
                break;
            case "bzip2":
            case "x-bzip2":
                info."request-body" = bunzip2_to_string(info."request-body");
                break;
            case "identity":
                info."request-body" = binary_to_string(info."request-body");
                break;
            case NOTHING:
                break;
            default:
                throw "UNKNOWN-CONTENT-ENCODING", sprintf("unknown Content-Encoding %y used to send message",
                    info.headers."Content-Encoding");
        }
        return trim(info."request-body");
    }

    static usage() {
        printf("usage: %s [options] get|put|post|delete URL ['qore msg body']
 -c,--cert=ARG             client X.509 certificate to use
 -E,--full-exception       show full exception output
 -e,--send-encoding[=ARG]  set compression in outgoing request
                           [ARG=gzip,deflate,bzip2,identity]
 -H,--header=ARG           send header with request
 -i,--ignore-errors        ignore HTTP errors and show the literal response in all cases
 -K,--keypass=ARG          password to use for key for client certificate
 -k,--key=ARG              key to use for client certificate
 -L,--list                 list URI paths and methods supported by the schema
 -l,--literal              show literal API calls (more l's, more info)
 -P,--proxy-url=ARG        set the proxy URL (ex: http://proxy:port)
 -p.--base-path=ARG        override the base path for Swagger schemas
 -R,--reformat             reformat data with -l+ for better readability
 -S,--serialization=ARG    set REST data serialization type:
                           auto, json, yaml, xml, rawxml, url
 -s,--swagger=ARG          use the given Swagger 2.0 schema for validation
 -T,--timeout=ARG          set HTTP timeout in seconds
 -t,--token=ARG            an authorization token to use
 -X,--lax-parsing          lax parsing of Swagger schemas
 -x,--example[=ARG]        generate an example message; ARG=
                             q: generate Qore example (default data)
                             r<code>: generate response (default request)
 -y,--token-type=ARG       the type of token (default: Bearer)
 -h,--help                 this help text
", get_script_name());
        exit(1);
    }

    static error(string fmt) {
        stderr.printf("%s: ERROR: %s\n", get_script_name(), vsprintf(fmt, argv));
        exit(1);
    }
}
