**FREE
//   A REST API that allows you to retrieve, create, update and
//   delete customer information.  Basically, a full maintenance
//   APIs written with YAJL.
//                              Scott Klement, March 2018
//
//   Can get a list of customers with GET to:
//      http://example.com/rest/custdetail/
//
//    Can GET, PUT, DELETE and specific customer
//       http://example.com/rest/custdetail/1234
//
//    Can POST to create a new customer
//       http://example.com/rest/custdetail/
//
//    The customer record is sent/received over the network
//    in JSON format like this:
//
//    {
//       "success": true,
//       "errorMsg": "Only used if success=false",
//       "data": {
//          "custno": 496,
//          "name": "Acme Foods",
//          "address": {
//             "street": "123 Main Street",
//             "city": "Boca Raton",
//             "state": "FL",
//             "postal": "12345-6789",
//          }
//       }
//    }
//
//    In the cast of a list, the "data" element above will
//    be an array.
//
//  Before compiling:
//    - Install YAJL, put it in your *LIBL
//    - Create the CUSTFILE file (see the CUSTFILE member)
//    - Create the NEXTCUST data area
//      CRTDTAARA DTAARA(NEXTCUST) TYPE(*DEC) LEN(5 0) VALUE(1)
//
//  To compile:
//    *> CRTSQLRPGI CUSTDETAIL SRCFILE(QRPGLESRC) DBGVIEW(*SOURCE)
//
//  To install in Apache, add these directives, and restart:
//
//    ScriptAliasMatch /rest/([a-z0-9]*)/.* /qsys.lib/yajlrest.lib/$1.pgm
//    <Directory /qsys.lib/yajlrest.lib>
//       SetEnv QIBM_CGI_LIBRARY_LIST "YAJL;MYDATA;MYSRV;YAJLREST"
//       require valid-user
//       AuthType basic
//       AuthName "REST APIs"
//       PasswdFile %%SYSTEM%%
//       UserId %%CLIENT%%
//    </Directory>
//
ctl-opt dftactgrp(*no) actgrp('KLEMENT') decedit('0.')
        bnddir('YAJL') option(*srcstmt: *nodebugio: *noshowcpy);

/include YAJL_H

dcl-c UPPER const('ABCDEFGHIJKLMNOPQRSTUVWXYZ');
dcl-c lower const('abcdefghijklmnopqrstuvwxyz');

dcl-s NEXTCUST packed(5: 0) dtaara;

dcl-ds CUSTFILE extname('CUSTFILE') qualified end-ds;

dcl-ds address_t qualified template;
   street varchar(30) inz('');
   city   varchar(20) inz('');
   state  char(2)     inz('  ');
   postal varchar(10) inz('');
end-ds;

dcl-ds data_t qualified template;
   custno packed(5: 0) inz(0);
   name varchar(30) inz('');
   address likeds(address_t) inz(*likeds);
end-ds;

dcl-ds cust_t qualified template;
   success ind inz(*on);
   errorMsg varchar(500) inz('');
   data likeds(data_t) inz(*likeds);
end-ds;

dcl-ds cust likeds(cust_t) inz(*likeds);

dcl-s  custid like(data_t.custno);
dcl-s errmsg varchar(500) inz('');
dcl-s method varchar(10);

exec SQL
   set option naming=*sys, commit=*none;

reset cust;

if getInput( method: custid: errmsg ) = *off;
   cust.success = *off;
   cust.errorMsg = errmsg;
   sendResponse(cust);
   return;
endif;

select;
when method = 'GET' and custid = 0;

   listCustomers();

when method = 'GET';

   loadDbRecord(custid: cust);
   sendResponse(cust);

when method = 'PUT';

   if loadDbRecord(custid: cust) = *on;
      if loadInputJson(cust) = *on;
         cust.data.custno = custid;
         updateDbRecord(cust);
      endif;
   endif;

   sendResponse(cust);

when method = 'POST';

   if loadInputJson(cust) = *on;
      writeDbRecord(cust);
   endif;

   sendResponse(cust);

when method = 'DELETE';

   if loadDbRecord(custid: cust) = *on;
      deleteDbRecord(cust);
   endif;

   sendResponse(cust);

endsl;

return;


// ------------------------------------------------------------------------
//   getInput():  Retrieve the basic HTTP input for this call
//
//      method = (output) HTTP method used (GET, POST, DELETE, PUT)
//      custid = (output) customer id, or 0 if none provided
//      errmsg = (output) error message that occurred (if any)
//
//   Returns *ON if successful, *OFF otherwise
// ------------------------------------------------------------------------

dcl-proc getInput;

   dcl-pi *n ind;
      method varchar(10);
      custid like(data_t.custno);
      errmsg varchar(500);
   end-pi;

   dcl-pr getenv pointer extproc(*dclcase);
      var pointer value options(*string);
   end-pr;

   dcl-c REQUIRED_PART const('/rest/custdetail/');

   dcl-s env pointer;
   dcl-s pos int(10);
   dcl-s custpart varchar(50);
   dcl-s url varchar(1000);

   errMsg = '';
   method = 'GET';
   url    = '';

   // ------------------------------------------------------
   // Retrieve the HTTP method.
   //  -  Default to GET if not provided
   // ------------------------------------------------------

   env = getenv('REQUEST_METHOD');
   if env <> *null;
      method = %xlate(lower: UPPER: %str(env));
   endif;

   // ------------------------------------------------------
   //  Retrieve the URL
   //   - Should always be provided!
   // ------------------------------------------------------

   env = getenv('REQUEST_URI');
   if env = *null;
      errMsg = 'Unable to retrieve URL';
      return *off;
   else;
      url = %xlate(UPPER: lower: %str(env));
   endif;

   // ------------------------------------------------------
   //   Extract the customer ID from the URL.
   //    - if not provided, set to 0
   //    - should always be provided for PUT/POST/DELETE
   // ------------------------------------------------------

   monitor;
      pos = %scan(REQUIRED_PART:url) + %len(REQUIRED_PART);
      custpart = %subst(url: pos);
      custid = %int(custpart);
   on-error;
      custid = 0;
   endmon;

   if custid = 0 and method <> 'GET' and method <> 'POST';
      errMsg = 'You must supply a customer ID!';
      return *off;
   endif;

   return *on;

end-proc;


// ------------------------------------------------------------------------
//   loadDbRecord():  Load customer database record
//
//   custid = (input) customer number to retrieve
//     cust = (output) customer record
//
//   returns *on if record loaded, *off otherwise
// ------------------------------------------------------------------------

dcl-proc loadDbRecord;

   dcl-pi *n ind;
      custid like(data_t.custno) const;
      cust   likeds(cust_t);
   end-pi;

   dcl-ds Rec extname('CUSTFILE') qualified end-ds;

   exec SQL
     select *
       into :Rec
       from CUSTFILE
      where custno = :custid;

   if %subst(sqlstt:1:2) <> '00' and %subst(sqlstt:1:2) <> '01';
      cust.success = *off;
      cust.errorMsg = 'Customer not found!';
      return *off;
   endif;

   cust.data.custno = rec.custno;
   cust.data.name   = rec.name;

   cust.data.address.street = rec.street;
   cust.data.address.city   = rec.city;
   cust.data.address.state  = rec.state;
   cust.data.address.postal = rec.postal;

   return *on;

end-proc;


// ------------------------------------------------------------------------
//  updateDbRecord():  Updates an existing customer record
//
//    cust = (i/o) customer information DS
//
//  returns *ON if successful, *OFF otherwise.
// ------------------------------------------------------------------------

dcl-proc updateDbRecord;

   dcl-pi *n ind;
      cust likeds(cust_t);
   end-pi;

   dcl-ds udata likeds(data_t);
   dcl-ds uaddress likeds(address_t);

   eval-corr udata = cust.data;
   eval-corr uaddress = cust.data.address;

   exec SQL
     update CUSTFILE
       set
          name   = :udata.Name,
          street = :uaddress.Street,
          city   = :uaddress.City,
          state  = :uaddress.state,
          postal = :uaddress.postal
       where
          custno = :udata.CustNo;

   if %subst(sqlstt:1:2)<>'00' and %subst(sqlstt:1:2)<>'01';
      cust.success = *off;
      cust.errorMsg = 'SQL State ' + sqlstt + ' updating CUSTFILE';
   endif;

   return cust.success;
end-proc;


// ------------------------------------------------------------------------
//  getNextCustno(): Gets the next available customer number from
//                   the data area.
//
//  For this to work, the NEXTCUST data area must exist. If you don't have
//  it, create it with:
//
//     CRTDTAARA DTAARA(your-lib/NEXTCUST) TYPE(*DEC) LEN(5 0) VALUE(1)
//
//  returns the next custno, or 0 upon failure
// ------------------------------------------------------------------------

dcl-proc getNextCustno;

   dcl-pi *n packed(5: 0);
   end-pi;

   dcl-s newCust packed(5: 0);

   monitor;

      in *lock NEXTCUST;

      newCust = NEXTCUST;

      if NEXTCUST = *hival;
         NEXTCUST = 1;
      else;
         NEXTCUST += 1;
      endif;

      out NEXTCUST;

   on-error;
      return 0;
   endmon;

   return newCust;

end-proc;


// ------------------------------------------------------------------------
//  writeDbRecord():  Creates a new customer record
//
//    cust = (i/o) customer information DS
//
//  returns *ON if successful, *OFF otherwise.
// ------------------------------------------------------------------------

dcl-proc writeDbRecord;

   dcl-pi *n ind;
      cust likeds(cust_t);
   end-pi;

   dcl-ds idata likeds(data_t);
   dcl-ds iaddress likeds(address_t);

   cust.data.custno = getNextCustno();
   if cust.data.custno = 0;
      cust.success = *off;
      cust.errorMsg = 'Unable to get next available customer number';
      return *off;
   endif;

   eval-corr idata = cust.data;
   eval-corr iaddress = cust.data.address;

   exec SQL
     insert into CUSTFILE
       (custno, name, street, city, state, postal)
       values( :idata.custno,    :idata.name,
               :iaddress.street, :iaddress.city,
               :iaddress.state,  :iaddress.postal );

   if %subst(sqlstt:1:2)<>'00' and %subst(sqlstt:1:2)<>'01';
      cust.success = *off;
      cust.errorMsg = 'SQL State ' + sqlstt + ' writing CUSTFILE';
   endif;

   return cust.success;
end-proc;


// ------------------------------------------------------------------------
//  deleteDbRecord():  Deletes the customer record if it exists
//
//    cust = (i/o) customer information DS
//
//  returns *ON if successful, *OFF otherwise.
// ------------------------------------------------------------------------

dcl-proc deleteDbRecord;

   dcl-pi *n ind;
      cust likeds(cust_t);
   end-pi;

   dcl-s custid packed(5: 0);

   custid = cust.data.custno;

   exec SQL
     delete from CUSTFILE
       where custno = :custid;

   if %subst(sqlstt:1:2) <> '00' and %subst(sqlstt:1:2) <> '01';
      cust.success = *off;
      cust.errorMsg = 'SQL state ' + sqlstt + ' deleting customer';
   endif;

   return cust.success;

end-proc;


// ------------------------------------------------------------------------
//  loadInputJson():  If a PUT or POST was requested (write/update)
//                    load the customer record provided by the consumer
//
//     cust = (i/o) customer info data structure.
//
//  returns *ON if successful, *OFF otherwise
// ------------------------------------------------------------------------

dcl-proc loadInputJson;

   dcl-pi *n ind;
      cust likeds(cust_t);
   end-pi;

   dcl-s docNode like(yajl_val);
   dcl-s node like(yajl_val);
   dcl-s dataNode like(yajl_val);
   dcl-s data like(yajl_val);
   dcl-s addrNode like(yajl_val);
   dcl-s errMsg varchar(500);
   dcl-s i int(10);
   dcl-s j int(10);
   dcl-s field varchar(50);


   //--------------------------------------------------
   //  get the JSON document sent from the consumer
   //--------------------------------------------------

   docNode = yajl_stdin_load_tree(*on: errMsg);
   if errMsg <> '';
      cust.errorMsg = 'json parse: ' + errMsg;
      cust.success = *off;
      return *off;
   endif;


   //--------------------------------------------------
   //  Load the success/errorMsg fields provided by
   //  consumer.
   //--------------------------------------------------

   node = yajl_object_find(docNode: 'success');
   if node = *null;
      cust.errorMsg = 'Required field "success" not found.';
      cust.success = *off;
      yajl_tree_free(docNode);
      return *off;
   endif;

   node = yajl_object_find(docNode: 'errorMsg');
   if node = *null;
      cust.errorMsg = '';
   else;
      cust.errorMsg = yajl_get_string(node);
   endif;

   if cust.success = *off;
      yajl_tree_free(docNode);
      return *off;
   endif;


   //--------------------------------------------------
   //  Load the customer information provided by
   //  the consumer.
   //--------------------------------------------------

   data = yajl_object_find(docNode: 'data');
   if data = *null;
      cust.errorMsg = 'Required field "data" not found.';
      cust.success = *off;
      yajl_tree_free(docNode);
      return *off;
   endif;

   i = 0;
   dow yajl_object_loop(data: i: field: dataNode);

      select;
      when field = 'custno';
         cust.data.custno = yajl_get_number(dataNode);

      when field = 'name';
         cust.data.name = yajl_get_string(dataNode);

      when field = 'address';

         j = 0;
         dow yajl_object_loop(dataNode: j: field: addrNode);

            select;
            when field = 'street';
               cust.data.address.street = yajl_get_string(addrNode);
            when field = 'city';
               cust.data.address.city = yajl_get_string(addrNode);
            when field = 'state';
               cust.data.address.state = yajl_get_string(addrNode);
            when field = 'postal';
               cust.data.address.postal = yajl_get_string(addrNode);
            endsl;

         enddo;

     endsl;

   enddo;

   yajl_tree_free(docNode);
   return *on;

end-proc;


// ------------------------------------------------------------------------
//  sendResponse():  Send the JSON response document
//
//    cust = (input) customer information DS
//
//  returns *ON if successful, *OFF otherwise.
// ------------------------------------------------------------------------

dcl-proc sendResponse;

   dcl-pi *n ind;
      cust likeds(cust_t) const;
   end-pi;

   dcl-s errMsg varchar(500) inz('');

   yajl_genOpen(*on);
   yajl_beginObj();

   yajl_addBool('success': cust.success);
   yajl_addChar('errorMsg': cust.errorMsg);

   if cust.success = *on;

      yajl_beginObj('data');

      yajl_addNum('custno': %char(cust.data.custno));
      yajl_addChar('name': %trim(cust.data.name));

      yajl_beginObj('address');
      yajl_addChar('street': %trim(cust.data.address.street));
      yajl_addChar('city':   %trim(cust.data.address.city  ));
      yajl_addChar('state':  %trim(cust.data.address.state ));
      yajl_addChar('postal': %trim(cust.data.address.postal));
      yajl_endObj();

      yajl_endObj();

   endif;

   yajl_endObj();

   if cust.success;
      yajl_writeStdout(200: errMsg);
   else;
      yajl_writeStdout(500: errMsg);
   endif;

   yajl_genClose();

   return (errMsg = '');

end-proc;


// ------------------------------------------------------------------------
//   Provide list of all customers (called when GET without any custno)
//
//   NOTE: Output is written directly to consumer
//
//   Returns *ON if successful, *OFF otherwise.
// ------------------------------------------------------------------------

dcl-proc listCustomers;

   dcl-pi *n ind;
   end-pi;

   dcl-s errmsg varchar(500);
   dcl-s success ind;

   dcl-ds CUSTLIST qualified;
      custno like(CUSTFILE.custno);
      name   like(CUSTFILE.name);
      street like(CUSTFILE.street);
      city   like(CUSTFILE.city);
      state  like(CUSTFILE.state);
      postal like(CUSTFILE.postal);
   end-ds;

   exec SQL declare custlist cursor for
     select custno, name, street, city, state, postal
       from custfile
      order by custno;

   exec SQL open custlist;
   exec SQL fetch next from custlist into :CUSTLIST;

   yajl_genOpen(*on);
   yajl_beginObj();

   success = *on;
   errmsg  = '';

   if %subst(sqlstt:1:2) <> '00' and %subst(sqlstt:1:2) <> '01';
      success = *off;
      errMsg = 'SQL State ' + sqlstt + ' querying customer list';
   endif;

   yajl_addBool('success': success);
   yajl_addChar('errorMsg': errmsg);

   if (success = *off);
      yajl_endObj();
      yajl_writeStdout(500: errMsg);
      yajl_genClose();
      return success;
   endif;

   yajl_beginArray('data');

   dow %subst(sqlstt:1:2)='00' or %subst(sqlstt:1:2)='01';

      yajl_beginObj();

      yajl_addNum('custno': %char(custlist.custno));
      yajl_addChar('name': %trim(custlist.name) );

      yajl_beginObj('address');
      yajl_addChar('street': %trim(custlist.street));
      yajl_addChar('city': %trim(custlist.city));
      yajl_addChar('state': custlist.state);
      yajl_addChar('postal': %trim(custlist.postal));
      yajl_endObj();

      yajl_endObj();

      exec SQL fetch next from custlist into :CUSTLIST;
   enddo;

   exec SQL close custlist;

   if (success);
      yajl_endArray();
   endif;

   yajl_endObj();
   yajl_writeStdout(200: errMsg);
   yajl_genClose();

   return success;

end-proc;


