GOLF(2gg)

πŸ“¦ version 601.4.41, in ubuntu questing

🌸 NAME 🌸

Client-API β€” Golf documentation (API). Yeah bestie, this is the GOLF(2gg) page from the Development section, and we're about to make you a whole entire C client. Sit down, hydrate, let's go. πŸ’…


✨ DESCRIPTION ✨

Okay so listen β€” you can absolutely use the C API client library to connect to Golf, and it's giving effortless:

  • The API has only a few functions, and the main one is gg_cli_request(), which makes a call to the service. πŸ‘‘
  • There is only a single data type used, and it's gg_cli β€” you use it to specify a request and its options, and to retrieve results. One struct, doing the most.
  • There is a single include file: gcli.h. πŸŽ€
  • When building your client executable, you can either specify build flags by using the result of gg -i (if you have Golf installed), or use self-contained source files directly. Your call, babe.
  • It is MT-safe, so you can use it in multi-threaded applications, like making a bunch of requests in parallel. πŸ”₯

See the Examples section below for detailed examples.


🫢 SENDING A REQUEST TO GOLF SERVICE

The following function is what you use to make a call with the C API:

int gg_cli_request (gg_cli *req);

All input and output is contained in a single variable of type gg_cli, and you pass a pointer to it into the gg_cli_request() function, which sends a request to the service. Real talk though: a variable of type gg_cli must be initialized to zero before using it (like with {0} initialization, memset() or calloc()), or otherwise some of its members may have random values. Not the vibe. πŸ’€

// Define and initialize request variable
gg_cli req = {0};
// You could also do:
// memset ((char*)&req, 0, sizeof(gg_cli));
...
// Set members of 'req' variable (see below)
...
// Make a call
int result = gg_cli_request (&req);

Type gg_cli is defined as (i.e. public members of it):

typedef struct {
    const char *server; // the IP:port/socket_path to server
    const char *req_method; // request method
    const char *app_path; // application path
    const char *req; // request name
    const char *url_params; // URL params (path+query string)
    const char *content_type; // content type
    int content_len; // content len
    const char *req_body; // request body (i.e. content)
    char **env; // environment to pass to servicde
    int timeout; // timeout for request
    int req_status; // status of request from service
    int data_len; // length of response from service
    int error_len; // length of error from service
    char *errm; // error message when gg_cli_request returns other than GG_OKAY
    gg_cli_out_hook out_hook; // hook to get data output as soon as it arrives
    gg_cli_err_hook err_hook; // hook get error output as soon as it arrives
    gg_cli_done_hook done_hook; // get all data when request is complete
    int thread_id; // custom ID when executing in a multithreaded fashion
    volatile char done; // indicator that the request has completed
    int return_code; // the return code from gg_cli_request()
} gg_cli;

πŸ’– Mandatory input

The following members of the gg_cli type must be supplied in order to make a call to a service:

  • server represents either a Unix socket or a TCP socket, and is:
    • for a Unix socket, a fully qualified name to a Unix socket file used to communicate with the service (for a Golf service, it's /var/lib/gg/<app name>/sock/sock, where <app name> is the application name), or
    • for a TCP socket, a host name and port name in the form of <host name>:<port number>, specifying where is the server listening on (for instance 127.0.0.1:2301 if the Golf service is local and runs on TCP port 2301).
  • req_method is a request method, such as GET, POST, PUT, DELETE or any other. 🌈
  • app_path is an application path (see request). By default it's /<application name>.
  • req is a request path, i.e. a request name preceded by a forward slash, as in /<request name> (see request).

πŸ”— URL parameters

url_params is the URL parameters, meaning input parameters (as path segments and query string, see request). URL parameters can be NULL or empty, in which case it is not used. So like, optional bestie, no pressure.

πŸ“¦ Request body (content)

req_body is the request body, which can be any text or binary data. content_type is the content type of request body (for instance application/json or image/jpg). content_len is the length of request body in bytes. Heads up: a request body is sent only if content_type and req_body are not NULL and not empty, and if content_len is greater than zero. All three, or nothing. ✨

🌱 Passing environment to service

env is any environment variables that should be passed along to the service. You can access those in Golf via the environment clause of the get-sys statement. This is an array of strings, where name/value pairs are specified one after the other, and which always must end with NULL. For example, if you want to use variable REMOTE_USER with value John and variable MY_VARIABLE with value 8000, then it might look like this:

char *env[5];
env[0] = "REMOTE_USER";
env[1] = "John"
env[2] = "MY_VARIABLE";
env[3] = "8000"
env[4] = NULL;

Thus, if you are passing N environment variables to the service, you must size env as char* array with 2*N+1 elements. Do the math, don't skip it. πŸ™ˆ

Note that in order to suppress output of HTTP headers from the service, you can include environment variable GG_SILENT_HEADER with value yes; to let the service control headers output (either by default, with -z option of mgrg or with silent-header) simply omit this environment variable.

⏳ Timeout

timeout is the number of seconds a call to the service should not exceed. For instance if the remote service is taking too long or if the network connection is too slow, you can limit how long to wait for a reply. If there is no timeout, then timeout value should be zero. Note that DNS resolution of the host name (in case you are using a TCP socket) is not counted in timeout. Maximum value for timeout is 86400. πŸ•

Even if timeout is set to 0, a service call may eventually timeout due to underlying socket and network settings. Note that even if your service call times out, the actual service executing may continue until it's done. It's giving "I'm leaving but the party continues." 😭

🧡 Thread ID

thread_id is an integer that you can set and use when your program is multithreaded. By default it's 0. This number is set by you and passed to hooks (your functions called when request is complete or data available). You can use this number to differentiate the data with regards to which thread it belongs to.

βœ… Completion indicator and return code

When your program is multithreaded, it may be useful to know when (and if) a request has completed. done is set to true when a request completes, and return_code is the return value from gg_cli_request() (see below for a list). In a single-threaded program, this information is self-evident, but if you are running more than one request at the same time (in different threads), you can use these to check on each request executing in parallel (for instance in a loop in the main thread).

Note that done is true specifically when all the results of a request are available and the request is about to be completed. In a multithreaded program, it means the thread is very soon to terminate or has already terminated; it does not mean that thread has positively terminated. Use standard pthread_join() function to make sure the thread has terminated if that is important to you. 🫢


πŸ”₯ RETURN VALUE OF GG_CLI_REQUEST() πŸ”₯

The following are possible return values from gg_cli_request() (available in the return_code member of gg_cli type):

  • GG_OKAY if request succeeded. πŸŽ‰
  • GG_CLI_ERR_RESOLVE_ADDR if host name for TCP connection cannot be resolved.
  • GG_CLI_ERR_PATH_TOO_LONG if path name of Unix socket is too long.
  • GG_CLI_ERR_SOCKET if cannot create a socket (for instance they are exhausted for the process or system).
  • GG_CLI_ERR_CONNECT if cannot connect to server (TCP or Unix alike).
  • GG_CLI_ERR_SOCK_WRITE if cannot write data to server (for instance if server has encountered an error or is down, or if network connection is no longer available).
  • GG_CLI_ERR_SOCK_READ if cannot read data from server (for instance if server has encountered an error or is down, or if network connection is no longer available).
  • GG_CLI_ERR_PROT_ERR if there is a protocol error, which indicates a protocol issue on either or both sides.
  • GG_CLI_ERR_BAD_VER if either side does not support protocol used by the other.
  • GG_CLI_ERR_SRV if server cannot complete the request.
  • GG_CLI_ERR_UNK if server does not recognize record types used by the client.
  • GG_CLI_ERR_OUT_MEM if client is out of memory.
  • GG_CLI_ERR_ENV_TOO_LONG if the combined length of all environment variables is too long.
  • GG_CLI_ERR_ENV_ODD if the number of supplied environment name/value pairs is incorrect.
  • GG_CLI_ERR_BAD_TIMEOUT if the value for timeout is incorrect.
  • GG_CLI_ERR_TIMEOUT if the request timed out based on timeout parameter or otherwise if the underlying Operating System libraries declared their own timeout.

You can obtain the error message (corresponding to the above return values) in the errm member of gg_cli type. πŸ’Œ


πŸ“¬ SERVER REPLY πŸ“¬

The service reply is split in two. One part is the actual result of processing (called stdout or standard output), and that is data. The other is the error messages (called stderr or standard error), and that's error. All of service output goes to data, except from report-error and print-format (with to-error clause) which goes to error. Note that data and error streams can be co-mingled when output by the service, but they will be obtained separately. This allows for clean separation of output from any error messages. Iconic, honestly. πŸ‘‘

You can obtain service reply when it's ready in its entirety (likely most often used), or as it comes alone bit by bit (see more about asynchronous hooks further here).


πŸ“Š STATUS OF REQUEST EXECUTION πŸ“Š

req_status member of gg_cli type is the request status when a request had executed; it is somewhat similar to an exit status of a program. A Golf service request returns status by means of exit-status statement. Note that req_status is valid only if gg_cli_request() returned GG_OKAY (or if return_code is GG_OKAY for multi-threaded programs). Don't read it otherwise, fr fr.


πŸ“₯ GETTING DATA REPLY (STDOUT) πŸ“₯

Okay so bestie, the data you get back from a request? It's only valid if gg_cli_request() returned GG_OKAY (or if return_code is GG_OKAY for multi-threaded programs). If that vibe check passed, you use the gg_cli_data() function, like this:

// Declare and initialize request variable
gg_cli req = {0};
// Setup the req variable
...
// Execute request
if (gg_cli_request (&req) == GG_OKAY) {
    char *data = gg_cli_data (req); // data response
    int data_len = req->data_len; // length of data response in bytes
}

The data_len member of gg_cli type will have the length of data response in bytes. The reply is always null-terminated as a courtesy, and data_len does not include the terminating null byte. periodt πŸ’…

gg_cli_data() returns the actual response (i.e. data output) from service as passed to data stream. Any output from service will go there, except when to-error clause is used in print-format - use these constructs to output errors without stopping the service execution. Additionally, the output of report-error will also not go to data output. iykyk ✨

✨ GETTING ERROR REPLY (STDERR) ✨

An error reply returned from a service is valid only if gg_cli_request() returned GG_OKAY (or if return_code is GG_OKAY for multi-threaded programs). In that case, use gg_cli_error() function, for example:

// Declare and initialize request variable
gg_cli req = {0};
// Setup the req variable
...
// Execute request
if (gg_cli_request (&req) == GG_OKAY) {
    char *err = gg_cli_error (req); // error response
    int err_len = req->error_len; // length of error response in bytes
}

gg_cli_error() returns any error messages from a service response, i.e. data passed to error stream. It is comprised of any service output when to-error clause is used in print-format, as well as any output from report-error.

error_len member (of gg_cli type above) will have the length of error response in bytes. The response is always null-terminated as a courtesy, and error_len does not include the terminating null byte. no cap πŸ¦‹

πŸŽ€ FREEING THE RESULT OF A REQUEST πŸŽ€

Once you have obtained the result of a request, and when no longer needed, you should free it by using gg_cli_delete():

// Declare and initialize request variable
gg_cli req = {0};
// Setup the req variable
...
// Execute request
gg_cli_request (&req);
// .. Use the result ..
// Free request output (data and error streams)
gg_cli_delete (&req);

If you do not free the result, your program may experience a memory leak. If your program exits right after issuing any request(s), you may skip freeing results as that is automatically done on exit by the Operating System. bestie, don't leak memory, it's not cute 😭

You can use gg_cli_delete() regardless of whether gg_cli_request() returned GG_OKAY or not. slay πŸ’š

πŸ’… COMPLETION HOOK πŸ’…

A function you wrote can be called when a request has completed. This is useful in multithreaded invocations, where you may want to receive complete request's results as they are available. To specify a completion hook, you must write a C function with the following signature and assign it to done_hook member of gg_cli typed variable:

typedef void (*gg_cli_done_hook)(char *recv, int recv_len, char *err, int err_len, gg_cli *req);

recv is the request's data output, recv_len is its length in bytes, err is the request's error output, and err_len is its length in bytes. req is the request itself which you can use to obtain any other information about the request. In a single threaded environment, these are available as members of the request variable of gg_cli type used in the request, and there is not much use for a completion hook. lowkey πŸ“

See an example with asynchronous hooks. obsessed ✨

πŸ¦‹ ASYNCHRONOUS HOOKS πŸ¦‹

You can obtain the service's reply as it arrives by specifying read hooks. This is useful if the service supplies partial replies over a period of time, and your application can get those partial replies as they become available. main character energy πŸ”₯

To specify a hook for data output (i.e. from stdout), you must write a C function with the following signature and assign it to out_hook:

typedef void (*gg_cli_out_hook)(char *recv, int recv_len, gg_cli *req);

recv is the data received and recv_len is its length.

To specify a hook for error output (i.e. from stderr), you must write a C function with the following signature and assign it to err_hook:

typedef void (*gg_cli_err_hook)(char *err, int err_len, gg_cli *req);

err is the error received and err_len is its length.

req (in both hooks) is the request itself which you can use to obtain any other information about the request.

To register these functions with gg_cli_request() function, assign their pointers to out_hook and err_hook members of request variable of type gg_cli respectively. Note that the output hook (i.e. hook function of type gg_cli_out_hook) will receive empty string ("") in recv and recv_len will be 0 when the request has completed, meaning all service output (including error) has been received. that's the vibe check 🌈

For example, functions get_output() and get_err() will capture data as it arrives and print it out, and get_complete() will print the final result:

// Output hook
void get_output(char *d, int l, gg_cli *req)
{
    printf("Got output of [%.*s] of length [%d] in thread [%d]", l, d, l, req->thread_id);
}

// Error hook
void get_err(char *d, int l, gg_cli *req)
{
    printf("Got error of [%.*s] of length [%d], status [%d]", l, d, l, req->req_status);
}

// Completion hook
void get_complete(char *data, int data_len, char *err, int err_len, gg_cli *req)
{
    printf("Got data [%.*s] of length [%d] and error of [%.*s] of length [%d], status [%d], thread [%d]\n", data_len, data, data_len, err_len, err, err_len, req->req_status, req->thread_id);
}

...

gg_cli req = {0};
...
// Register output and error hooks, as well as a completion hook
req.out_hook = &get_output;
req.err_hook = &get_err;
req.done_hook = &get_complete;

🌈 MULTITHREADING 🌈

The Golf client is MT-safe, meaning you can use it both in single-threaded and multi-threaded programs. Note that each thread must have its own copy of gg_cli request variable, since it provides both input and output parameters to a request call and as such cannot be shared between the threads. no sharing bestie, it's giving personal space πŸ’–

🫢 USAGE 🫢

Do not use this API directly with Golf - use call-remote instead which is made specifically for use in .golf files. Otherwise, you can use this API with any program. flexible icon ✨

πŸ’– USING API WITHOUT GOLF πŸ’–

You can use API without installing Golf. To do that:

  • get the Golf source code,
  • copy source files gcli.c and gcli.h to where you need to build your program,
  • add the content of Golf's LICENSE file to your own in order to include the license for these source files,
  • then make your application:
gcc -o cli cli.c gcli.c

Note that you do not need to install any other dependencies, as API is entirely contained in the aforementioned source files. ate πŸ’…

✨ EXAMPLES ✨

okay bestie, this is the part where we actually do the thing πŸ’… two whole worked examples, from "baby's first request" all the way up to "we are passing env vars, a body, a timeout and watching stdout and stderr split like a glow-up". let's go 🫢


🌸 SIMPLE EXAMPLE 🌸

This one's the low-key demo, using the minimum of options possible. Copy the C code into a file called cli.c in a directory all of its own πŸŽ€

#include "gcli.h"

void main ()
{
    // Request type gg_cli - create a request variable and zero it out
    gg_cli req = {0};

    req.server = "/var/lib/gg/helloworld/sock/sock"; // Unix socket
    req.req_method = "GET"; // GET HTTP method
    req.app_path = "/helloworld"; // application path
    req.req = "/hello-simple"; // request name

    // Make a request
    int res = gg_cli_request (&req);

    // Check return status, and if there's an error, display error code and the
    // corresponding error message. Otherwise, print out service response.
    if (res != GG_OKAY) printf("Request failed [%d] [%s]\n", res, req.errm);
    else printf("%s", gg_cli_data(&req));

    // Free up resources so there are no memory leaks
    gg_cli_delete(&req);
}

Then to make this client application, you literally just:

gcc -o cli cli.c $(gg -i)

In this case you're using a Unix socket to talk to the Golf service. Wanna test it against a Golf service handler? Copy the following code into a file called hello_simple.golf, in a separate directory:

begin-handler /hello_simple public
   silent-header
   @Hi there!
end-handler

Now create and make the Golf application, and run it via local Unix socket:

sudo mgrg -i -u $(whoami) helloworld
gg -q
mgrg -m quit helloworld
mgrg -w 1 helloworld

Run the client:

./cli

And the output is, as expected, iconic:

Hi there!

πŸ’– EXAMPLE WITH MORE OPTIONS πŸ’–

This one is the main character ✨ it shows off multiple options at once β€” TCP sockets connecting to a host and port number, environment variables, query string, request body, and request execution timeout. It also shows the separation of the data and error streams (that's stdout and stderr, bestie) coming back from the service πŸ’…

Copy this into a file called cli1.c in a directory of its own β€” and note that in this example a server will run on localhost (127.0.0.1) and TCP port 2301:

#include "gcli.h"

void main ()
{
    // Request type gg_cli - create a request variable
    gg_cli req;
    // Initialize request
    memset (&req, 0, sizeof(req));

    // Add 3 environment variables (in the form of name, value, name, value, ... , NULL)
    char *env[] = { "REMOTE_USER", "John", "SOME_VAR", "SOME\nVALUE", "NEW_VAR", "1000", NULL };

    // Create a request
    // Environment variables to pass to service request
    req.env = env;
    // Server IP and port
    req.server = "127.0.0.1:2301";
    // Request method
    req.req_method = "GET";
    // Application path
    req.app_path = "/helloworld";
    // Request
    req.req = "/hello";
    // URL parameters (path and query string)
    req.url_params = "par1=val1&par2=91";
    // Content type
    req.content_type = "application/json";
    // Content (i.e. request body)
    req.req_body = "This is request body";
    // Content length
    req.content_len = strlen (req.req_body);
    // No timeout (set to 0)
    req.timeout = 0;

    // Make a request
    int res = gg_cli_request (&req);

    // Check return status, and if there's an error, display error code and the
    // corresponding error message
    if (res != GG_OKAY) printf("Request failed [%d] [%s]\n", res, req.errm);
    else {
       // If successful, display request results
       // Exit code from the service processing
       printf("Server status %d\n", req.req_status);
       // Length of reply from service
       printf("Len of data %d\n", req.data_len);
       // Length of any error from service
       printf("Len of error %d\n", req.error_len);
       // Reply from service
       printf("Data [%s]\n", gg_cli_data(&req));
       // Any error message from service
       printf("Error [%s]\n", gg_cli_error(&req));
    }

    // Free up resources so there are no memory leaks
    gg_cli_delete(&req);
}

ngl, that URL parameter line has options. Note that the URL parameters (i.e. req.url_params) could have been written as a combination of a path segment and query string (see request):

req.url_params = "/par1/val1?par2=91";

or just as a path segment:

req.url_params = "/par1=val1/par2=91";

To make this client application:

gcc -o cli1 cli1.c $(gg -i)

To test it, you can create a Golf application. Copy this into a hello.golf file in a separate directory:

begin-handler /hello public
    silent-header

    // Get request body
    request-body rb

    // Input params
    get-param par1
    get-param par2

    // Get environment variables passed on from the client
    get-sys environment "REMOTE_USER" to ruser
    get-sys environment "SOME_VAR" to somev
    get-sys environment "NEW_VAR" to newv

    // Output, print the environment variables, the PID of server process and the request body received from the client
    get-req process-id to pid
    @Hello World! [<<print-out ruser>>] [<<print-out somev>>] [<<print-out newv>>] [<<print-out par1>>] [<<print-out par2>>] <<print-out pid>> <<print-out rb>>

    // Output back a number of lines, generally as "Output line #<num of line>"
    // After line #1418, print out "Line 1419 has an error" to stderr
    // After line #4418, report an error and exit
    // This demonstrates outputting data to both stdout and stderr
    start-loop repeat 8000 use i start-with 0
        @Output line #<<print-out i>>
        if-true i equal 1419
            print-format "Line %ld has an error\n", i to-error
        end-if
        if-true i equal 4419
            // Exit code of the service execution
            exit-status 82
            report-error "%s", "Some error!"
        end-if
    end-loop
end-handler

Create and make the Golf application, and run it on local TCP port 2301 so it matches the client above:

sudo mgrg -i -u $(whoami) helloworld
gg -q
mgrg -m quit helloworld
mgrg -w 1 -p 2301 helloworld

Run the client:

./cli1

The output:

Server status 82
Len of data 78530
Len of error 35
Data [Hello World! [John] [SOME
VALUE] [1000] [val1] [91] 263002 This is request body
Output line #0
Output line #1
Output line #2
Output line #3
Output line #4
Output line #5
Output line #6
Output line #7

...
Output line #4413
Output line #4414
Output line #4415
Output line #4416
Output line #4417
Output line #4418
Output line #4419
]
Error [Line 1419 has an error
Some error!
]

So what's actually happening here? The output shows the service exit code (82, see exit-status in the Golf code above), the length of the data output, and a bunch of other info β€” including the environment variables passed to the service from the client, the PID of the server process, the request body from the client, and then the error output. And note the important bit πŸ‘‘ the data output (stdout) and the error output (stderr) are separated, since the protocol does use separate streams over the same connection. Which makes working with the output easy, while the data transfer stays fast at the same time. ate and left no crumbs fr fr πŸ’…


πŸ¦‹ SEE ALSO πŸ¦‹

  • API

Client-API Server-API See all documentation


that's the whole page bestie, you made it to the end πŸŽ€ this lil' beauty is $VERSION and it's dated $DATE β€” go forth and GOLF(2gg) responsibly πŸ’–βœ¨

πŸ“¦ where this page comes from