From 8ddb86785cb9bff8358738bcba336bfeeea388cc Mon Sep 17 00:00:00 2001 From: Allan Bowe Date: Wed, 18 Aug 2021 23:45:45 +0300 Subject: [PATCH] feat: new fcmp stpsrv_header function --- all.sas | 129 +++++++++++++++++- base/mf_existfunction.sas | 4 +- fcmp/mcf_stpsrv_header.sas | 94 +++++++++++++ .../crossplatform/mcf_stpsrv_header.test.sas | 39 ++++++ tests/crossplatform/mcf_string2file.test.sas | 2 +- 5 files changed, 265 insertions(+), 3 deletions(-) create mode 100644 fcmp/mcf_stpsrv_header.sas create mode 100644 tests/crossplatform/mcf_stpsrv_header.test.sas diff --git a/all.sas b/all.sas index de7d752..49f955b 100644 --- a/all.sas +++ b/all.sas @@ -147,6 +147,40 @@ options noquotelenmax; %end; %mend mf_existfileref;/** + @file + @brief Checks if a function exists + @details Returns 1 if the function exists, else 0. Note that this function + can be slow as it needs to open the sashelp.vfuncs table. + + Usage: + + %put %mf_existfunction(CAT); + %put %mf_existfunction(DOG); + + Full credit to [Bart](https://sasensei.com/user/305) for the vfunc pointer + and the tidy approach for pure macro data set filtering. + Check out his [SAS Packages](https://github.com/yabwon/SAS_PACKAGES) + framework! + + @param [in] name (positional) - function name + + @author Allan Bowe +**/ +/** @cond */ + +%macro mf_existfunction(name +)/*/STORE SOURCE*/; + + %local dsid rc exist; + %let dsid=%sysfunc(open(sashelp.vfunc(where=(fncname="%upcase(&name)")))); + %let exist = %sysfunc(fetch(&dsid, NOSET)); + %let rc = %sysfunc(close(&dsid)); + + %sysevalf(0 = &exist) + +%mend mf_existfunction; + +/** @endcond *//** @file @brief Checks if a variable exists in a data set. @details Returns 0 if the variable does NOT exist, and return the position of @@ -18805,12 +18839,105 @@ run; %mend ml_json; /** + @file + @brief Provides a replacement for the stpsrv_header function + @details The stpsrv_header is normally a built-in function, used to set the + headers for SAS 9 Stored Processes as documented here: + https://go.documentation.sas.com/doc/en/itechcdc/9.4/stpug/srvhead.htm + + The purpose of this custom function is to provide a replacement when running + similar code as a web service against + [sasjs/server](https://github.com/sasjs/server). It operates by creating a + text file with the headers. The location of this text file is determined by + a macro variable (`sasjs_stpsrv_header_loc`) which needs to be injected into + each service by the calling process, eg: + + %let sasjs_stpsrv_header_loc = C:/temp/some_uuid/stpsrv_header.txt; + + Note - the function works by appending headers to the file. If multiple same- + named headers are provided, they will all be appended - the calling process + needs to pick up the last one. This will mean removing the attribute if the + final record has an empty value. + + The function takes the following (positional) parameters: + + | PARAMETER | DESCRIPTION | + |------------|-------------| + | name $ | name of the header attribute to create| + | value $ | value of the header attribute| + + It returns 0 if successful, or -1 if an error occured. + + Usage: + + %let sasjs_stpsrv_header_loc=%sysfunc(pathname(work))/stpsrv_header.txt; + + %mcf_stpsrv_header(wrap=YES, insert_cmplib=YES) + + data _null_; + rc=stpsrv_header('Content-type','application/text'); + rc=stpsrv_header('Content-disposition',"attachment; filename=file.txt"); + run; + + data _null_; + infile "&sasjs_stpsrv_header_loc"; + input; + putlog _infile_; + run; + + + @param [out] wrap= (NO) Choose YES to add the proc fcmp wrapper. + @param [out] insert_cmplib= (NO) Choose YES to insert the package into the + CMPLIB reference. + @param [out] lib= (work) The output library in which to create the catalog. + @param [out] cat= (sasjs) The output catalog in which to create the package. + @param [out] pkg= (utils) The output package in which to create the function. + Uses a 3 part format: libref.catalog.package + +**/ + +%macro mcf_stpsrv_header(wrap=NO + ,insert_cmplib=NO + ,lib=WORK + ,cat=SASJS + ,pkg=UTILS +)/*/STORE SOURCE*/; + +%if &wrap=YES %then %do; + proc fcmp outcat=&lib..&cat..&pkg; +%end; + +function stpsrv_header(name $, value $); + length loc $128 val $512; + loc=symget('sasjs_stpsrv_header_loc'); + val=trim(name)!!': '!!value; + length fref $8; + rc=filename(fref,loc); + if (rc ne 0) then return( -1 ); + fid = fopen(fref,'a'); + if (fid = 0) then return( -1 ); + rc=fput(fid, val); + rc=fwrite(fid); + rc=fclose(fid); + rc=filename(fref); + return(0); +endsub; + +%if &wrap=YES %then %do; + quit; +%end; + +%if &insert_cmplib=YES %then %do; + options insert=(CMPLIB=(&lib..&cat)); +%end; + +%mend mcf_stpsrv_header;/** @file @brief Adds a string to a file @details Creates an fcmp function for appending a string to an external file. If the file does not exist, it is created. - The function itself takes the following paramters: + The function itself takes the following (positional) parameters: | PARAMETER | DESCRIPTION | |------------|-------------| diff --git a/base/mf_existfunction.sas b/base/mf_existfunction.sas index 937ced2..f875d48 100644 --- a/base/mf_existfunction.sas +++ b/base/mf_existfunction.sas @@ -1,7 +1,9 @@ /** @file @brief Checks if a function exists - @details Returns 1 if the function exists, else 0 + @details Returns 1 if the function exists, else 0. Note that this function + can be slow as it needs to open the sashelp.vfuncs table. + Usage: %put %mf_existfunction(CAT); diff --git a/fcmp/mcf_stpsrv_header.sas b/fcmp/mcf_stpsrv_header.sas new file mode 100644 index 0000000..3f00d17 --- /dev/null +++ b/fcmp/mcf_stpsrv_header.sas @@ -0,0 +1,94 @@ +/** + @file + @brief Provides a replacement for the stpsrv_header function + @details The stpsrv_header is normally a built-in function, used to set the + headers for SAS 9 Stored Processes as documented here: + https://go.documentation.sas.com/doc/en/itechcdc/9.4/stpug/srvhead.htm + + The purpose of this custom function is to provide a replacement when running + similar code as a web service against + [sasjs/server](https://github.com/sasjs/server). It operates by creating a + text file with the headers. The location of this text file is determined by + a macro variable (`sasjs_stpsrv_header_loc`) which needs to be injected into + each service by the calling process, eg: + + %let sasjs_stpsrv_header_loc = C:/temp/some_uuid/stpsrv_header.txt; + + Note - the function works by appending headers to the file. If multiple same- + named headers are provided, they will all be appended - the calling process + needs to pick up the last one. This will mean removing the attribute if the + final record has an empty value. + + The function takes the following (positional) parameters: + + | PARAMETER | DESCRIPTION | + |------------|-------------| + | name $ | name of the header attribute to create| + | value $ | value of the header attribute| + + It returns 0 if successful, or -1 if an error occured. + + Usage: + + %let sasjs_stpsrv_header_loc=%sysfunc(pathname(work))/stpsrv_header.txt; + + %mcf_stpsrv_header(wrap=YES, insert_cmplib=YES) + + data _null_; + rc=stpsrv_header('Content-type','application/text'); + rc=stpsrv_header('Content-disposition',"attachment; filename=file.txt"); + run; + + data _null_; + infile "&sasjs_stpsrv_header_loc"; + input; + putlog _infile_; + run; + + + @param [out] wrap= (NO) Choose YES to add the proc fcmp wrapper. + @param [out] insert_cmplib= (NO) Choose YES to insert the package into the + CMPLIB reference. + @param [out] lib= (work) The output library in which to create the catalog. + @param [out] cat= (sasjs) The output catalog in which to create the package. + @param [out] pkg= (utils) The output package in which to create the function. + Uses a 3 part format: libref.catalog.package + +**/ + +%macro mcf_stpsrv_header(wrap=NO + ,insert_cmplib=NO + ,lib=WORK + ,cat=SASJS + ,pkg=UTILS +)/*/STORE SOURCE*/; + +%if &wrap=YES %then %do; + proc fcmp outcat=&lib..&cat..&pkg; +%end; + +function stpsrv_header(name $, value $); + length loc $128 val $512; + loc=symget('sasjs_stpsrv_header_loc'); + val=trim(name)!!': '!!value; + length fref $8; + rc=filename(fref,loc); + if (rc ne 0) then return( -1 ); + fid = fopen(fref,'a'); + if (fid = 0) then return( -1 ); + rc=fput(fid, val); + rc=fwrite(fid); + rc=fclose(fid); + rc=filename(fref); + return(0); +endsub; + +%if &wrap=YES %then %do; + quit; +%end; + +%if &insert_cmplib=YES %then %do; + options insert=(CMPLIB=(&lib..&cat)); +%end; + +%mend mcf_stpsrv_header; \ No newline at end of file diff --git a/tests/crossplatform/mcf_stpsrv_header.test.sas b/tests/crossplatform/mcf_stpsrv_header.test.sas new file mode 100644 index 0000000..5b87d57 --- /dev/null +++ b/tests/crossplatform/mcf_stpsrv_header.test.sas @@ -0,0 +1,39 @@ +/** + @file + @brief Testing mcf_stpsrv_header macro + +

SAS Macros

+ @li mcf_stpsrv_header.sas + @li mp_assert.sas + +**/ + +%let sasjs_stpsrv_header_loc=%sysfunc(pathname(work))/stpsrv_header.txt; + +%mcf_stpsrv_header(wrap=YES, insert_cmplib=YES) + +data _null_; + rc=stpsrv_header('Content-type','application/text'); + rc=stpsrv_header('Content-disposition',"attachment; filename=file.txt"); +run; + +%let test1=FAIL; +%let test2=FAIL; + +data _null_; + infile "&sasjs_stpsrv_header_loc"; + input; + if _n_=1 and _infile_='Content-type: application/text' + then call symputx('test1','PASS'); + else if _n_=2 & _infile_='Content-disposition: attachment; filename=file.txt' + then call symputx('test2','PASS'); +run; + +%mp_assert( + iftrue=(%str(&test1)=%str(PASS)), + desc=Check first header line +) +%mp_assert( + iftrue=(%str(&test2)=%str(PASS)), + desc=Check second header line +) \ No newline at end of file diff --git a/tests/crossplatform/mcf_string2file.test.sas b/tests/crossplatform/mcf_string2file.test.sas index 7b58bac..4de900e 100644 --- a/tests/crossplatform/mcf_string2file.test.sas +++ b/tests/crossplatform/mcf_string2file.test.sas @@ -1,6 +1,6 @@ /** @file - @brief Testing mm_webout macro + @brief Testing mcf_string2file macro

SAS Macros

@li mcf_string2file.sas