From 97204c8a0eb213af5f4c656cd30f1ecf82c8a520 Mon Sep 17 00:00:00 2001 From: "E. Scott Daniels" Date: Mon, 29 Jun 2020 15:39:57 -0400 Subject: [PATCH] Add json support The munchkin xAPP needs lightweight json, as might other xAPPs, so it makes sense to do the work onece and add it to the framework. Issue-ID: RIC-420 Signed-off-by: E. Scott Daniels Change-Id: I20ea999ba77566d1404f0d69be0f63669943236e --- .gitmodules | 3 + CHANGES | 5 +- CMakeLists.txt | 27 +- doc/src/lib/rst.im | 2 +- doc/src/lib/setup.im | 26 ++ doc/src/rtd/rel-notes.rst | 7 + doc/src/rtd/rel-notes.xfm | 6 +- doc/src/user/.gitignore | 2 + doc/src/user/Makefile | 6 +- doc/src/user/cpp_frame.im | 4 +- doc/src/user/jhash.im | 255 ++++++++++++ doc/src/user/user_guide.xfm | 15 +- docs/rel-notes.rst | 7 + docs/user-guide.rst | 976 +++++++++++++++++++++++--------------------- ext/jsmn | 1 + src/json/CMakeLists.txt | 42 ++ src/json/README | 11 + src/json/jhash.cpp | 283 +++++++++++++ src/json/jhash.hpp | 86 ++++ src/json/jwrapper.c | 807 ++++++++++++++++++++++++++++++++++++ src/json/jwrapper.h | 76 ++++ test/Makefile | 12 +- test/jhash_test.cpp | 268 ++++++++++++ test/scrub_gcov.sh | 1 - test/test.json | 50 +++ test/unit_test.sh | 24 +- test/ut_support.cpp | 69 ++++ 27 files changed, 2587 insertions(+), 484 deletions(-) create mode 100644 .gitmodules create mode 100644 doc/src/user/jhash.im create mode 160000 ext/jsmn create mode 100644 src/json/CMakeLists.txt create mode 100644 src/json/README create mode 100644 src/json/jhash.cpp create mode 100644 src/json/jhash.hpp create mode 100644 src/json/jwrapper.c create mode 100644 src/json/jwrapper.h create mode 100644 test/jhash_test.cpp create mode 100644 test/test.json create mode 100644 test/ut_support.cpp diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a88d7ab --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "ext/jsmn"] + path = ext/jsmn + url = https://github.com/zserge/jsmn diff --git a/CHANGES b/CHANGES index 74ab3b7..d7bd724 100644 --- a/CHANGES +++ b/CHANGES @@ -1,9 +1,12 @@ # This file contains a brief summary of each version bump. +2020 June 29; version 1.2.0 + Add support for json parsing + 2020 June 26; version 1.1.0 Change the port type in constructors to indicate "const" - Version bump to 1.1.0 to allow patches to bronze code to + Version bump to 1.1.0 to allow patches to bronze code to continue to be done on the 1.0.* level. 2020 April 28; version 1.0.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index eaeef37..9ae759e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,7 +29,7 @@ project( ricxfcpp ) cmake_minimum_required( VERSION 3.5 ) set( major_version "1" ) # should be automatically populated from git tag later, but until CI process sets a tag we use this -set( minor_version "1" ) +set( minor_version "2" ) set( patch_level "0" ) set( install_root "${CMAKE_INSTALL_PREFIX}" ) @@ -153,8 +153,7 @@ endif() # but Cmake insists on having these exist when we add them to include directories to # enable code to find them after we build them. # -include_directories( "${CMAKE_CURRENT_SOURCE_DIR}/src/messaging" ) - +include_directories( "${CMAKE_CURRENT_SOURCE_DIR}/src/messaging;${CMAKE_CURRENT_SOURCE_DIR}/src/json;${CMAKE_CURRENT_SOURCE_DIR}/ext/jsmn" ) # Compiler flags # @@ -170,31 +169,43 @@ else() endif() unset( GPROF CACHE ) # we don't want this to persist + +# --------------------- external building -------------------------------------------------------- +execute_process( COMMAND git submodule update --init -- ext/jsmn + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + + +# --------------------- framework building -------------------------------------------------------- # Include modules +add_subdirectory( src/json ) add_subdirectory( src/messaging ) add_subdirectory( src/xapp ) #add_subdirectory( doc ) # this will auto skip if {X}fm is not available - # shared and static libraries are built from the same object files. # -add_library( ricxfcpp_shared SHARED "$;$" ) +add_library( ricxfcpp_shared SHARED + "$;$;$" +) set_target_properties( ricxfcpp_shared PROPERTIES OUTPUT_NAME "ricxfcpp" SOVERSION ${major_version} - VERSION ${major_version}.${minor_version}.${patch_level} + VERSION ${major_version}.${minor_version}.${patch_level} ) target_include_directories( ricxfcpp_shared PUBLIC "src/messenger" "src/xapp" ) # we only build/export the static archive (.a) if generating a dev package if( DEV_PKG ) - add_library( ricxfcpp_static STATIC "$;$" ) + add_library( ricxfcpp_static STATIC + "$;$;$" + ) set_target_properties( ricxfcpp_static PROPERTIES OUTPUT_NAME "ricxfcpp" SOVERSION ${major_version} - VERSION ${major_version}.${minor_version}.${patch_level} + VERSION ${major_version}.${minor_version}.${patch_level} ) target_include_directories( ricxfcpp_static PUBLIC "src/messenger" "src/xapp" ) endif() diff --git a/doc/src/lib/rst.im b/doc/src/lib/rst.im index e63a901..5d6da09 100644 --- a/doc/src/lib/rst.im +++ b/doc/src/lib/rst.im @@ -107,7 +107,7 @@ .dv item .br &lic .dv li .br &lic - .dv ex_start .sp 1 ^:^: .br .ll -2 .in +2 .nf + .dv ex_start .sp 1 ^:^: .sp 1 .ll -2 .in +2 .nf .dv ex_end .fo on .in -2 .ll +2 .sp 1 .dv proto_start .sp 1 .cc .5i .st 9 .sf Courier-bold .nf diff --git a/doc/src/lib/setup.im b/doc/src/lib/setup.im index a99d6d7..07d4f7c 100644 --- a/doc/src/lib/setup.im +++ b/doc/src/lib/setup.im @@ -42,6 +42,32 @@ .gv e OUTPUT_TYPE ot .im &{lib}/&{ot}.im + .gv e XFM_PASS pass + .dv pass &{pass!1} + + + .** define a 'subroutine' to add a variable and place it into the forward reference file + .** fref_sub.ca contains code to execute by the set_fref macor and fref.ca contains the + .** forward references to include during pass2. The macro set_ref will define the variable + .** passed as $1 (name) with the value passed as $2 and will add it to the fref.ca file. + .** +.ca shift start fref_sub.ca + .ca expand extend fref.ca + .dv &vname &vval + .ca end +.ca end + .dv set_fref .dv vname $1 ^: .dv vval $2 ^: .dv $1 $2 ^: .im fref_sub.ca + + + .if &pass 2 = + .im fref.ca + .ei + .** initialise the forward ref file + .ca start fref.ca + .** forward reference variables; auto generated +.ca end + .fi + .** set up for an index when using pfm and snare file is defined .if pfm diff --git a/doc/src/rtd/rel-notes.rst b/doc/src/rtd/rel-notes.rst index 7c5615b..0be1017 100644 --- a/doc/src/rtd/rel-notes.rst +++ b/doc/src/rtd/rel-notes.rst @@ -18,6 +18,13 @@ The following is a list of release highlights for the C++ xAPP Framework. +2020 June 29; version 1.2.0 +--------------------------- + +Add support for json parsing + + + 2020 June 26; version 1.1.0 --------------------------- diff --git a/doc/src/rtd/rel-notes.xfm b/doc/src/rtd/rel-notes.xfm index 8049fb6..370cda7 100644 --- a/doc/src/rtd/rel-notes.xfm +++ b/doc/src/rtd/rel-notes.xfm @@ -13,11 +13,15 @@ &h1(C++ Framework Release Notes) The following is a list of release highlights for the C++ xAPP Framework. +&h2(2020 June 29; version 1.2.0) + Add support for json parsing +&space + &h2(2020 June 26; version 1.1.0) Change the port type in constructors to indicate "const" &space - Version bump to 1.1.0 to allow patches to bronze code to + Version bump to 1.1.0 to allow patches to bronze code to continue to be done on the 1.0.* level. &space diff --git a/doc/src/user/.gitignore b/doc/src/user/.gitignore index aa1acd1..8ef4aae 100644 --- a/doc/src/user/.gitignore +++ b/doc/src/user/.gitignore @@ -3,3 +3,5 @@ *.txt *.html *.sp +*.ca +*.toc diff --git a/doc/src/user/Makefile b/doc/src/user/Makefile index 729b7fb..ca3c252 100644 --- a/doc/src/user/Makefile +++ b/doc/src/user/Makefile @@ -19,7 +19,7 @@ docs = user_guide src = user_guide.xfm -imbed_src = cpp_frame.im example1.im example2.im example3.im +imbed_src = cpp_frame.im example1.im example2.im example3.im jhash.im desired_out = rst ps # use care: the output type is used to source the macros based on the type @@ -37,7 +37,7 @@ desired_out = rst ps %.rst: %.xfm OUTPUT_TYPE=rst XFM_PASS=1 tfm $< /dev/null - GEN_TITLE=1 OUTPUT_TYPE=rst XFM_PASS=2 tfm $< | sed 's/^ //' >$@ + GEN_TITLE=1 OUTPUT_TYPE=rst XFM_PASS=2 tfm $< | sed 's/^ //; s/ *$$//' >$@ %.txt: %.xfm OUTPUT_TYPE=txt XFM_PASS=1 tfm $< /dev/null @@ -55,7 +55,7 @@ user_guide.ps: user_guide.xfm $(imbed_src) $(docs:%=%.rst): always publish: user_guide.rst - cp *.rst ../../../docs/ + cp user_guide.rst ../../../docs/user-guide.rst # intermeidate junk that might straggle clean: diff --git a/doc/src/user/cpp_frame.im b/doc/src/user/cpp_frame.im index aa4b166..9481b5a 100644 --- a/doc/src/user/cpp_frame.im +++ b/doc/src/user/cpp_frame.im @@ -41,7 +41,7 @@ parameters: &half_space &indent &beg_dlist(.5i:&ditemtext) - &ditem(port) A C string (char *) which defines the port that RMR will open to listen for connections. + &ditem(port) A C string (pointer to char) which defines the port that RMR will open to listen for connections. &half_space &ditem(wait) A Boolean value which indicates whether or not the initialization process should wait for @@ -208,7 +208,7 @@ Sending the message is a function of the message object itself and can take one &half_space &indent -&beg_list &lic1 +&beg_list(&lic1) &item Replying to the sender of a received message &half_space diff --git a/doc/src/user/jhash.im b/doc/src/user/jhash.im new file mode 100644 index 0000000..d381542 --- /dev/null +++ b/doc/src/user/jhash.im @@ -0,0 +1,255 @@ +.** vim: sw=4 ts=4 et : +.if false +================================================================================== + Copyright (c) 2020 Nokia + Copyright (c) 2020 AT&T Intellectual Property. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +================================================================================== +.fi + + +.if false + This imbed file contains the portion of the document that describes the + json support that is provided by the framework. +.fi + + +&h1(Json Support) +The C++ xAPP framework provides a very lightweight json parser and data +hash facility. +Briefly, a json hash (Jhash) can be established by creating an instance of +the Jhash object with a string of valid json. +The resulting object's functions can then be used to read values from the +resulting hash. + + +&h2(Creating The Jhash Object) +The Jhash object is created simply by passing a json string to the constructor. + +.cc 10l +&ex_start + #include + + std::string jstring = "{ \"tag\": \"Hello World\" }"; + Jhash* jh; + + jh = new Jhash( jstring.c_str() ); +&ex_end +&figure(The creation of the Jhash object.) +&space + + +Once the Jhash object has been created any of the methods described in the following +paragraphs can be used to retrieve the data: + +&h2(Json Blobs) +Json objects can be nested, and the nesting is supported by this representation. +The approach taken by Jhash is a "directory view" approach, where the "current directory," +or current &ital(blob,) limits the scope of visible fields. + +&space +As an example, the json contained in figure jblob_fig, contains a "root" blob and +two &ital(sub-blobs) (address and lease_info). + +&ex_start + { + "lodge_name": "Water Buffalo Lodge 714", + "member_count": 41, + "grand_poobah": "Larry K. Slate", + "attendance": [ 23, 14, 41, 38, 24 ], + "address": { + "street": "16801 Stonway Lane", + "suite": null, + "city": "Bedrock", + "post_code": "45701" + }, + "lease_info": { + "owner": "Stonegate Properties", + "amount": 216.49, + "due": "monthly", + "contact:" "Kyle Limestone" + } + } +&ex_end +.gv fig +.sv _fig +&set_fref(jblob_fig:&_fig) +&figure(Sample json with a root and too blobs.) + +&space +Upon creation of the Jhash object, the &ital(root) fields, &cw(lodge_name,) &cw(member_count,) and +&cw(grand_poobah) are immediately available. +The fields in the &ital(sub-blobs) are avalable only when the correct blob is selected. +The code sample in figure &fig_blob_sample illustrates how a &ital(sub-blob) is selected. + +&ex_start + jh->Set_blob( (char *) "address" ); // select address + jh->Unset_blob(); // return to root + jh->Set_blob( (char *) "lease_info" ); // slect the lease blob +&ex_end +.gv fig +.sv _fig +&set_fref(fig_blob_sample:&_fig) +&figure(Blob selection example.) +&space + +Currently, the selected blob must be unset in order to select a blob at the root +level; unset always sets the root blob. +Attempting to use the &cw(Set_blob) function will attempt to select the named blob +from the current blob, and not the root. + +&h2(Simple Value Extraction) +Simple values are the expected data types &ital(string, value,) and &ital(boolean.) +This lightweight json parser treats all values as floating point numbers and does not +attempt to maintain a separate integer type. +A fourth type, &ital(null,) is supported to allow the user to expressly check for +a field which is defined but has no value; as opposed to a field that was completely +missing from the data. +The following are the prototypes for the functions which allow values to be extracted: + +&half_space +&ex_start + std::string String( const char* name ); + float Value( const char* name ); + bool Bool( const char* name ); +&ex_end +&space + +Each of these funcitons returns the value associated with the field with the given &ital(name.) +If the value is missing, the following default values are returned: + +&half_space +&indent +&beg_dlist( 1i Helvetica-bold : : 15,80 ) + &di(String:) An empty string (.e.g ""). + &di(Value:) Zero (e.g 0.0) + &di(bool:) false +&end_dlist +&uindent +&space + +If the user needs to disambiguate between a missing value and the default value either the +&cw(Missing) or &cw(Exists) function should be used first. + +&h2(Testing For Existing and Missing Fields) +Two functions allow the developer to determine whether or not a field is included in the +json. +Both of these functions work on the current &ital(blob,) therefore it is important to ensure +that the correct blob is selected before using either of these funcitons. +The prototpyes for the &cw(Exists) and &cw(Missing) functions are below: + +&ex_start + bool Exists( const char* name ); + bool Is_missing( const char* name ); +&ex_end + +The &cw(Exists) function returns &ital(true) if the field name exists in the json and &ital(false) otherwise. +Conversly, the &cw(Missing) funciton returns &ital(true) when the field name does not exist in the json. + + +&h2(Testing Field Type) +The &cw(Exists) and &cw(Missing) functions might not be enough for the user code to validate +the data that it has. +To assist with this, several functions allow direct type testing on a field in the current +blob. +The following are the prototypes for these functions: + +&ex_start + bool Is_bool( const char* name ); + bool Is_null( const char* name ); + bool Is_string( const char* name ); + bool Is_value( const char* name ); +&ex_end + +&space +Each of these funcitons return &ital(true) if the field with the given name is of the type +being tested for. + + +&h2(Arrays) +Arrays are supported in the same manner as simple field values with the addition of the need +to supply an array index when fetching values from the object. +In addition, there is a &ital(length) function which can be used to determine the number +of elements in the named array. +The prototypes for the array based functions are below: + +&ex_start + int Array_len( const char* name ); + + bool Is_bool_ele( const char* name, int eidx ); + bool Is_null_ele( const char* name, int eidx ); + bool Is_string_ele( const char* name, int eidx ); + bool Is_value_ele( const char* name, int eidx ); + + bool Bool_ele( const char* name, int eidx ); + std::string String_ele( const char* name, int eidx ); + float Value_ele( const char* name, int eidx ); +&ex_end +&space + +For each of these functions the &cw(eidx) is the zero based element index which is to +be tested or selected. + +&h3(Arrays of Blobs) +An array containing blobs, rather than simiple field value pairs, the blob must +be selected prior to using it, just as a sub-blob needed to be selected. +The &cw(Set_blob_ele) function is used to do this and has the following prototype: + +&ex_start + bool Set_blob_ele( const char* name, int eidx ); +&ex_end +&space + +As with selecting a sub-blob, an unset must be preformed before selecting the next blob. +Figure &array_blob_code_fig illustrates how these functions can be used to read and print +values from the json in figure &array_blob_json_fig. + + +.cc 8l +&ex_start + "members": [ + { "name": "Fred Flinstone", "member_num": 42 }, + { "name": "Barney Rubble", "member_num": 48 }, + { "name": "Larry K Slate", "member_num": 22 }, + { "name": "Kyle Limestone", "member_num": 49 } + ] +&ex_end +.gv fig +&set_fref(array_blob_code_fig:&_fig) +&figure(Json array containing blobs.) +&space + + +.cc 18l +&ex_start + std::string mname; + float mnum; + int len; + + len = jh->Array_len( (char *) "members" ); + for( i = 0; i < len; i++ ) { + jh->Set_blob_ele( (char *) "members", i ); // select blob + + mname = jh->String( (char *) "name" ); // read values + mnum = jh->Value( (char *) "member_num" ); + fprintf( stdout, "%s is member %d\n", mname.c_str(), (int) mnum ); + + jh->Unset_blob(); // back to root + } +&ex_end +.gv fig +&set_fref(array_blob_json_fig:&_fig) +&figure(Code to process the array of blobs.) +&space + diff --git a/doc/src/user/user_guide.xfm b/doc/src/user/user_guide.xfm index c34538f..6df8248 100644 --- a/doc/src/user/user_guide.xfm +++ b/doc/src/user/user_guide.xfm @@ -34,8 +34,12 @@ .dv col_width 6.5i .dv fname_base user_guide -.dv doc_title RIC xAPP C++ Framework -.dv doc_subtitle User's Guide +.if pfm + .dv doc_title RIC xAPP C++ Framework + .dv doc_subtitle User's Guide +.ei + .dv doc_title User's Guide +.fi .dv orig_date 21 April 2020 .dv textfont Helvetica @@ -45,11 +49,12 @@ .ll 6i .hn off -.im ../lib/setup.im - -.im &{lib}/front_junk.im +.im ../lib/setup.im +.im &{lib}/front_junk.im +.** ------ major sections -------- .im cpp_frame.im +.im hash.im &h1(Example Programmes) The following sections contain several example programmes which are written on diff --git a/docs/rel-notes.rst b/docs/rel-notes.rst index 7c5615b..0be1017 100644 --- a/docs/rel-notes.rst +++ b/docs/rel-notes.rst @@ -18,6 +18,13 @@ The following is a list of release highlights for the C++ xAPP Framework. +2020 June 29; version 1.2.0 +--------------------------- + +Add support for json parsing + + + 2020 June 26; version 1.1.0 --------------------------- diff --git a/docs/user-guide.rst b/docs/user-guide.rst index a4ab5e9..58ccd58 100644 --- a/docs/user-guide.rst +++ b/docs/user-guide.rst @@ -1,470 +1,466 @@ - - -.. This work is licensed under a Creative Commons Attribution 4.0 International License. -.. SPDX-License-Identifier: CC-BY-4.0 -.. -.. CAUTION: this document is generated from source in doc/src/* -.. To make changes edit the source and recompile the document. -.. Do NOT make changes directly to .rst or .md files. - - -============================================================================================ -RIC xAPP C++ Framework -============================================================================================ --------------------------------------------------------------------------------------------- -User's Guide --------------------------------------------------------------------------------------------- - -Introduction -============================================================================================ - -The C++ framework allows the programmer to create an xApp -object instance, and to use that instance as the logic base. -The xApp object provides a message level interface to the RIC -Message Router (RMR), including the ability to register -callback functions which the instance will drive as messages -are received; much in the same way that an X-windows -application is driven by the window manager for all activity. -The xApp may also choose to use its own send/receive loop, -and thus is not required to use the callback driver mechanism -provided by the framework. - -The Framework API -============================================================================================ - -The C++ framework API consists of the creation of the xApp -object, and invoking desired functions via the instance of -the object. The following paragraphs cover the various steps -involved to create an xApp instance, wait for a route table -to arrive, send a message, and wait for messages to arrive. - -Creating the xApp instance --------------------------------------------------------------------------------------------- - -The creation of the xApp instance is as simple as invoking -the object's constructor with two required parameters: - - - - port - - A C string (pointer to char) which defines the port that RMR will - open to listen for connections. - - - wait - - A Boolean value which indicates whether or not the - initialization process should wait for the arrival of a - valid route table before completing. When true is - supplied, the initialization will not complete until RMR - has received a valid route table (or one is located via - the RMR_SEED_RT environment variable). - - - The following code sample illustrates the simplicity of - creating the instance of the xApp object. - - - :: - +.. This work is licensed under a Creative Commons Attribution 4.0 International License. +.. SPDX-License-Identifier: CC-BY-4.0 +.. +.. CAUTION: this document is generated from source in doc/src/* +.. To make changes edit the source and recompile the document. +.. Do NOT make changes directly to .rst or .md files. + + +============================================================================================ +User's Guide +============================================================================================ + + +INTRODUCTION +============ + +The C++ framework allows the programmer to create an xApp +object instance, and to use that instance as the logic base. +The xApp object provides a message level interface to the RIC +Message Router (RMR), including the ability to register +callback functions which the instance will drive as messages +are received; much in the same way that an X-windows +application is driven by the window manager for all activity. +The xApp may also choose to use its own send/receive loop, +and thus is not required to use the callback driver mechanism +provided by the framework. + + +THE FRAMEWORK API +================= + +The C++ framework API consists of the creation of the xApp +object, and invoking desired functions via the instance of +the object. The following paragraphs cover the various steps +involved to create an xApp instance, wait for a route table +to arrive, send a message, and wait for messages to arrive. + + +Creating the xApp instance +-------------------------- + +The creation of the xApp instance is as simple as invoking +the object's constructor with two required parameters: + + + port + A C string (pointer to char) which defines the port that + RMR will open to listen for connections. + + wait + A Boolean value which indicates whether or not the + initialization process should wait for the arrival of a + valid route table before completing. When true is + supplied, the initialization will not complete until RMR + has received a valid route table (or one is located via + the RMR_SEED_RT environment variable). + + The following code sample illustrates the simplicity of + creating the instance of the xApp object. + + + :: + #include #include int main( ) { std::unique_ptr xapp; char* listen_port = (char *) "4560"; //RMR listen port bool wait4table = true; // wait for a route table + xapp = std::unique_ptr( new Xapp( listen_port, wait4table ) ); } - - - Figure 1: Creating an xAPP instance. - - From a compilation perspective, the following is the simple - compiler invocation string needed to compile and link the - above program (assuming that the sample code exists in a file - called man_ex1.cpp. - - - :: - + + Figure 1: Creating an xAPP instance. + + From a compilation perspective, the following is the simple + compiler invocation string needed to compile and link the + above program (assuming that the sample code exists in a file + called man_ex1.cpp. + + + :: + g++ man_ex1.cpp -o man_ex1 -lricxfcpp -lrmr_si -lpthread - - - - The above program, while complete and capable of being - compiled, does nothing useful. When invoked, RMR will be - initialized and will begin listening for a route table; - blocking the return to the main program until one is - received. When a valid route table arrives, initialization - will complete and the program will exit as there is no code - following the instruction to create the object. - -Listening For Messages -============================================================================================ - - The program in the previous example can be extended with just - a few lines of code to enable it to receive and process - messages. The application needs to register a callback - function for each message type which it desires to process. - - Once registered, each time a message is received the - registered callback for the message type will be invoked by - the framework. - -Callback Signature --------------------------------------------------------------------------------------------- - - As with most callback related systems, a callback must have a - well known function signature which generally passes event - related information and a "user" data pointer which was - registered with the function. The following is the prototype - which callback functions must be defined with: - - - :: - + + + The above program, while complete and capable of being + compiled, does nothing useful. When invoked, RMR will be + initialized and will begin listening for a route table; + blocking the return to the main program until one is + received. When a valid route table arrives, initialization + will complete and the program will exit as there is no code + following the instruction to create the object. + + +LISTENING FOR MESSAGES +====================== + + The program in the previous example can be extended with just + a few lines of code to enable it to receive and process + messages. The application needs to register a callback + function for each message type which it desires to process. + + Once registered, each time a message is received the + registered callback for the message type will be invoked by + the framework. + + +Callback Signature +------------------ + + As with most callback related systems, a callback must have a + well known function signature which generally passes event + related information and a "user" data pointer which was + registered with the function. The following is the prototype + which callback functions must be defined with: + + + :: + void cb_name( Message& m, int mtype, int subid, int payload_len, Msg_component payload, void* usr_data ); - - - Figure 2: Callback function signature - - The parameters passed to the callback function are as - follows: &multi_space - - - m - - A reference to the Message that was received. - - - mtype - - The message type (allows for disambiguation if the - callback is registered for multiple message types). - - - subid - - The subscription ID from the message. - - - payload len - - The number of bytes which the sender has placed into the - payload. - - - payload - - A direct reference (smart pointer) to the payload. (The - smart pointer is wrapped in a special class in order to - provide a custom destruction function without burdening - the xApp developer with that knowledge.) - - - user data - - A pointer to user data. This is the pointer that was - provided when the function was registered. - - - To illustrate the use of a callback function, the previous - code example has been extended to add the function, register - it for message types 1000 and 1001, and to invoke the Run() - function in the framework (explained in the next section). - - :: - + + Figure 2: Callback function signature + + The parameters passed to the callback function are as + follows: &multi_space + + m + A reference to the Message that was received. + + mtype + The message type (allows for disambiguation if the + callback is registered for multiple message types). + + subid + The subscription ID from the message. + + payload len + The number of bytes which the sender has placed into the + payload. + + payload + A direct reference (smart pointer) to the payload. (The + smart pointer is wrapped in a special class in order to + provide a custom destruction function without burdening + the xApp developer with that knowledge.) + + user data + A pointer to user data. This is the pointer that was + provided when the function was registered. + + To illustrate the use of a callback function, the previous + code example has been extended to add the function, register + it for message types 1000 and 1001, and to invoke the Run() + function in the framework (explained in the next section). + + :: + #include #include long m1000_count = 0; // message counters, one for each type long m1001_count = 0; + // callback function that will increase the appropriate counter void cbf( Message& mbuf, int mtype, int subid, int len, Msg_component payload, void* data ) { long* counter; + if( (counter = (long *) data) != NULL ) { (*counter)++; } } + int main( ) { std::unique_ptr xapp; char* listen_port = (char *) "4560"; bool wait4table = false; + xapp = std::unique_ptr( new Xapp( listen_port, wait4table ) ); + // register the same callback function for both msg types xapp->Add_msg_cb( 1000, cbf, (void *) &m1000_count ); xapp->Add_msg_cb( 1001, cbf, (void *) &m1001_count ); + xapp->Run( 1 ); // start the callback driver } - - - Figure 3: Callback function example. - - As before, the program does nothing useful, but now it will - execute and receive messages. For this example, the same - function can be used to increment the appropriate counter - simply by providing a pointer to the counter as the user data - when the callback function is registered. In addition, a - subtle change from the previous example has been to set the - wait for table flag to false. - - For an xApp that is a receive only application (never sends) - it is not necessary to wait for RMR to receive a table from - the Route Manager. - -Registering A Default Callback --------------------------------------------------------------------------------------------- - - The xApp may also register a default callback function such - that the function will be invoked for any message that does - not have a registered callback. If the xAPP does not register - a default callback, any message which cannot be mapped to a - known callback function is silently dropped. A default - callback is registered by providing a *generic* message type - of xapp->DEFAULT_CALLBACK on an Add_msg_cb call. - -The Framework Callback Driver --------------------------------------------------------------------------------------------- - - The Run() function within the Xapp object is invoked to start - the callback driver, and the xApp should not expect the - function to return under most circumstances. The only - parameter that the Run() function expects is the number of - threads to start. For each thread requested, the framework - will start a listener thread which will allow received - messages to be processed in parallel. If supplying a value - greater than one, the xApp must ensure that the callback - functions are thread safe as it is very likely that the same - callback function will be invoked concurrently from multiple - threads. - -Sending Messages -============================================================================================ - - It is very likely that most xApps will need to send messages - and will not operate in "receive only" mode. Sending the - message is a function of the message object itself and can - take one of two forms: - - - - $1 Replying to the sender of a received message - - $1 Sending a message (routed based on the message type and subscription ID) - - - When replying to the sender, the message type and - subscription ID are not used to determine the destination of - the message; RMR ensures that the message is sent back to the - originating xApp. The xApp may still need to change the - message type and/or the subscription ID in the message prior - to using the reply function. - - To provide for both situations, two reply functions are - supported by the Message object as illustrated with the - following prototypes. - - - :: - + + Figure 3: Callback function example. + + As before, the program does nothing useful, but now it will + execute and receive messages. For this example, the same + function can be used to increment the appropriate counter + simply by providing a pointer to the counter as the user data + when the callback function is registered. In addition, a + subtle change from the previous example has been to set the + wait for table flag to false. + + For an xApp that is a receive only application (never sends) + it is not necessary to wait for RMR to receive a table from + the Route Manager. + + +Registering A Default Callback +------------------------------ + + The xApp may also register a default callback function such + that the function will be invoked for any message that does + not have a registered callback. If the xAPP does not register + a default callback, any message which cannot be mapped to a + known callback function is silently dropped. A default + callback is registered by providing a *generic* message type + of xapp->DEFAULT_CALLBACK on an Add_msg_cb call. + + +The Framework Callback Driver +----------------------------- + + The Run() function within the Xapp object is invoked to start + the callback driver, and the xApp should not expect the + function to return under most circumstances. The only + parameter that the Run() function expects is the number of + threads to start. For each thread requested, the framework + will start a listener thread which will allow received + messages to be processed in parallel. If supplying a value + greater than one, the xApp must ensure that the callback + functions are thread safe as it is very likely that the same + callback function will be invoked concurrently from multiple + threads. + + +SENDING MESSAGES +================ + + It is very likely that most xApps will need to send messages + and will not operate in "receive only" mode. Sending the + message is a function of the message object itself and can + take one of two forms: + + + + Replying to the sender of a received message + + + Sending a message (routed based on the message type and subscription ID) + + + When replying to the sender, the message type and + subscription ID are not used to determine the destination of + the message; RMR ensures that the message is sent back to the + originating xApp. The xApp may still need to change the + message type and/or the subscription ID in the message prior + to using the reply function. + + To provide for both situations, two reply functions are + supported by the Message object as illustrated with the + following prototypes. + + + :: + bool Send_response( int mtype, int subid, int response_len, std:shared_ptr response ); + bool Send_response( int response_len, std::shared_ptr response ); - - - Figure 4: Reply function prototypes. - - In the first prototype the xApp must supply the new message - type and subscription ID values, where the second function - uses the values which are currently set in the message. - Further, the new payload contents, and length, are supplied - to both functions; the framework ensures that the message is - large enough to accommodate the payload, reallocating it if - necessary, and copies the response into the message payload - prior to sending. Should the xApp need to change either the - message type, or the subscription ID, but not both, the - NO_CHANGE constant can be used as illustrated below. - - - :: - + + Figure 4: Reply function prototypes. + + In the first prototype the xApp must supply the new message + type and subscription ID values, where the second function + uses the values which are currently set in the message. + Further, the new payload contents, and length, are supplied + to both functions; the framework ensures that the message is + large enough to accommodate the payload, reallocating it if + necessary, and copies the response into the message payload + prior to sending. Should the xApp need to change either the + message type, or the subscription ID, but not both, the + NO_CHANGE constant can be used as illustrated below. + + + :: + msg->Send_response( Message::NO_CHANGE, Message::NO_SUBID, pl_length, (unsigned char *) payload ); - - - Figure 5: Send response prototype. - - In addition to the two function prototypes for - Send_response() there are two additional prototypes which - allow the new payload to be supplied as a shared smart - pointer. The other parameters to these functions are - identical to those illustrated above, and thus are not - presented here. - - The Send_msg() set of functions supported by the Message - object are identical to the Send_response() functions and are - shown below. - - - :: - + + Figure 5: Send response prototype. + + In addition to the two function prototypes for + Send_response() there are two additional prototypes which + allow the new payload to be supplied as a shared smart + pointer. The other parameters to these functions are + identical to those illustrated above, and thus are not + presented here. + + The Send_msg() set of functions supported by the Message + object are identical to the Send_response() functions and are + shown below. + + + :: + bool Send_msg( int mtype, int subid, int payload_len, std::shared_ptr payload ); + bool Send_msg( int mtype, int subid, int payload_len, unsigned char* payload ); + bool Send_msg( int payload_len, std::shared_ptr payload ); + bool Send_msg( int payload_len, unsigned char* payload ); - - - Figure 6: Send function prototypes. - - Each send function accepts the message, copies in the payload - provided, sets the message type and subscription ID (if - provided), and then causes the message to be sent. The only - difference between the Send_msg() and Send_response() - functions is that the destination of the message is selected - based on the mapping of the message type and subscription ID - using the current routing table known to RMR. - -Direct Payload Manipulation --------------------------------------------------------------------------------------------- - - For some applications, it might be more efficient to - manipulate the payload portion of an Xapp Message in place, - rather than creating it and relying on a buffer copy when the - message is finally sent. To achieve this, the xApp must - either use the smart pointer to the payload passed to the - callback function, or retrieve one from the message using - Get_payload() when working with a message outside of a - callback function. Once the smart pointer is obtained, the - pointer's get() function can be used to directly reference - the payload (unsigned char) bytes. - - When working directly with the payload, the xApp must take - care not to write more than the actual payload size which can - be extracted from the Message object using the - Get_available_size() function. - - When sending a message where the payload has been directly - altered, and no extra buffer copy is needed, a NULL pointer - should be passed to the Message send function. The following - illustrates how the payload can be directly manipulated and - returned to the sender (for simplicity, there is no error - handling if the payload size of the received message isn't - large enough for the response string, the response is just - not sent). - - - :: - + + Figure 6: Send function prototypes. + + Each send function accepts the message, copies in the payload + provided, sets the message type and subscription ID (if + provided), and then causes the message to be sent. The only + difference between the Send_msg() and Send_response() + functions is that the destination of the message is selected + based on the mapping of the message type and subscription ID + using the current routing table known to RMR. + + +Direct Payload Manipulation +--------------------------- + + For some applications, it might be more efficient to + manipulate the payload portion of an Xapp Message in place, + rather than creating it and relying on a buffer copy when the + message is finally sent. To achieve this, the xApp must + either use the smart pointer to the payload passed to the + callback function, or retrieve one from the message using + Get_payload() when working with a message outside of a + callback function. Once the smart pointer is obtained, the + pointer's get() function can be used to directly reference + the payload (unsigned char) bytes. + + When working directly with the payload, the xApp must take + care not to write more than the actual payload size which can + be extracted from the Message object using the + Get_available_size() function. + + When sending a message where the payload has been directly + altered, and no extra buffer copy is needed, a NULL pointer + should be passed to the Message send function. The following + illustrates how the payload can be directly manipulated and + returned to the sender (for simplicity, there is no error + handling if the payload size of the received message isn't + large enough for the response string, the response is just + not sent). + + + :: + Msg_component payload; // smart reference int pl_size; // max size of payload + payload = msg->Get_payload(); pl_size = msg->Get_available_size(); if( snprintf( (char *) payload.get(), pl_size, "Msg Received\\n" ) < pl_size ) { msg->Send_response( M_TYPE, SID, strlen( raw_pl ), NULL ); } - - - Figure 7: Send message without buffer copy. - - -Sending Multiple Responses --------------------------------------------------------------------------------------------- - - It is likely that the xApp will wish to send multiple - responses back to the process that sent a message that - triggered the callback. The callback function may invoke the - Send_response() function multiple times before returning. - - After each call, the Message retains the necessary - information to allow for a subsequent invocation to send more - data. It should be noted though, that after the first call to - {Send_response() the original payload will be lost; if - necessary, the xApp must make a copy of the payload before - the first response call is made. - -Message Allocation --------------------------------------------------------------------------------------------- - - Not all xApps will be "responders," meaning that some xApps - will need to send one or more messages before they can expect - to receive any messages back. To accomplish this, the xApp - must first allocate a message buffer, optionally initialising - the payload, and then using the message's Send_msg() function - to send a message out. The framework's Alloc_msg() function - can be used to create a Message object with a desired payload - size. - -Framework Provided Callbacks -============================================================================================ - - The framework itself may provide message handling via the - driver such that the xApp might not need to implement some - message processing functionality. Initially, the C++ - framework will provide a default callback function to handle - the RMR based health check messages. This callback function - will assume that if the message was received, and the - callback invoked, that all is well and will reply with an OK - state. If the xApp should need to override this simplistic - response, all it needs to do is to register its own callback - function for the health check message type. - -Example Programmes -============================================================================================ - - The following sections contain several example programmes - which are written on top of the C++ framework. - -RMR Dump xAPP --------------------------------------------------------------------------------------------- - - The RMR dump application is an example built on top of the - C++ xApp framework to both illustrate the use of the - framework, and to provide a useful diagnostic tool when - testing and troubleshooting xApps. - - The RMR dump xApp isn't a traditional xApp inasmuch as its - goal is to listen for message types and to dump information - about the messages received to the TTY much as tcpdump does - for raw packet traffic. The full source code, and Makefile, - are in the examples directory of the C++ framework repo. - - When invoked, the RMR dump program is given one or more - message types to listen for. A callback function is - registered for each, and the framework Run() function is - invoked to drive the process. For each recognised message, - and depending on the verbosity level supplied at program - start, information about the received message(s) is written - to the TTY. If the forwarding option, -f, is given on the - command line, and an appropriate route table is provided, - each received message is forwarded without change. This - allows for the insertion of the RMR dump program into a flow, - however if the ultimate receiver of a message needs to reply - to that message, the reply will not reach the original - sender, so RMR dump is not a complete "middle box" - application. - - The following is the code for this xAPP. Several functions, - which provide logic unrelated to the framework, have been - omitted. The full code is in the framework repository. - - - - :: - + + Figure 7: Send message without buffer copy. + + + +Sending Multiple Responses +-------------------------- + + It is likely that the xApp will wish to send multiple + responses back to the process that sent a message that + triggered the callback. The callback function may invoke the + Send_response() function multiple times before returning. + + After each call, the Message retains the necessary + information to allow for a subsequent invocation to send more + data. It should be noted though, that after the first call to + {Send_response() the original payload will be lost; if + necessary, the xApp must make a copy of the payload before + the first response call is made. + + +Message Allocation +------------------ + + Not all xApps will be "responders," meaning that some xApps + will need to send one or more messages before they can expect + to receive any messages back. To accomplish this, the xApp + must first allocate a message buffer, optionally initialising + the payload, and then using the message's Send_msg() function + to send a message out. The framework's Alloc_msg() function + can be used to create a Message object with a desired payload + size. + + +FRAMEWORK PROVIDED CALLBACKS +============================ + + The framework itself may provide message handling via the + driver such that the xApp might not need to implement some + message processing functionality. Initially, the C++ + framework will provide a default callback function to handle + the RMR based health check messages. This callback function + will assume that if the message was received, and the + callback invoked, that all is well and will reply with an OK + state. If the xApp should need to override this simplistic + response, all it needs to do is to register its own callback + function for the health check message type. + + +EXAMPLE PROGRAMMES +================== + + The following sections contain several example programmes + which are written on top of the C++ framework. + + +RMR Dump xAPP +------------- + + The RMR dump application is an example built on top of the + C++ xApp framework to both illustrate the use of the + framework, and to provide a useful diagnostic tool when + testing and troubleshooting xApps. + + The RMR dump xApp isn't a traditional xApp inasmuch as its + goal is to listen for message types and to dump information + about the messages received to the TTY much as tcpdump does + for raw packet traffic. The full source code, and Makefile, + are in the examples directory of the C++ framework repo. + + When invoked, the RMR dump program is given one or more + message types to listen for. A callback function is + registered for each, and the framework Run() function is + invoked to drive the process. For each recognised message, + and depending on the verbosity level supplied at program + start, information about the received message(s) is written + to the TTY. If the forwarding option, -f, is given on the + command line, and an appropriate route table is provided, + each received message is forwarded without change. This + allows for the insertion of the RMR dump program into a flow, + however if the ultimate receiver of a message needs to reply + to that message, the reply will not reach the original + sender, so RMR dump is not a complete "middle box" + application. + + The following is the code for this xAPP. Several functions, + which provide logic unrelated to the framework, have been + omitted. The full code is in the framework repository. + + + + :: + #include #include #include + #include "ricxfcpp/xapp.hpp" + /* Information that the callback needs outside of what is given to it via parms on a call @@ -478,7 +474,9 @@ RMR Dump xAPP std::atomic icount; // messages ignored std::atomic hdr; // number of messages before next header } cb_info_t; + // ---------------------------------------------------------------------- + /* Dump bytes to tty. */ @@ -486,17 +484,20 @@ RMR Dump xAPP int i; int j; char cheater[17]; + fprintf( stdout, " 0000 | " ); j = 0; for( i = 0; i < len; i++ ) { cheater[j++] = isprint( buf[i] ) ? buf[i] : '.'; fprintf( stdout, "%02x ", buf[i] ); + if( j == 16 ) { cheater[j] = 0; fprintf( stdout, " | %s\\n %04x | ", cheater, i+1 ); j = 0; } } + if( j ) { i = 16 - (i % 16); for( ; i > 0; i-- ) { @@ -506,13 +507,16 @@ RMR Dump xAPP fprintf( stdout, " | %s\\n", cheater ); } } + /* generate stats when the hdr count reaches 0. Only one active thread will ever see it be exactly 0, so this is thread safe. */ void stats( cb_info_t& cbi ) { int curv; // current stat trigger value + curv = cbi.hdr--; + if( curv == 0 ) { // stats when we reach 0 fprintf( stdout, "ignored: %ld processed: %ld\\n", cbi.icount.load(), cbi.pcount.load() ); @@ -520,30 +524,38 @@ RMR Dump xAPP fprintf( stdout, "\\n %5s %5s %2s %5s\\n", "MTYPE", "SUBID", "ST", "PLLEN" ); } + cbi.hdr = cbi.stats_freq; // reset must be last } } + void cb1( Message& mbuf, int mtype, int subid, int len, Msg_component payload, void* data ) { cb_info_t* cbi; long total_count; + if( (cbi = (cb_info_t *) data) == NULL ) { return; } + cbi->pcount++; stats( *cbi ); // gen stats & header if needed + if( cbi->vlevel > 0 ) { fprintf( stdout, " %-5d %-5d %02d %-5d \\n", mtype, subid, mbuf.Get_state(), len ); + if( cbi->vlevel > 1 ) { dump( payload.get(), len > 64 ? 64 : len ); } } + if( cbi->forward ) { // forward with no change to len or payload mbuf.Send_msg( Message::NO_CHANGE, NULL ); } } + /* registered as the default callback; it counts the messages that we aren't giving details about. @@ -551,16 +563,20 @@ RMR Dump xAPP void cbd( Message& mbuf, int mtype, int subid, int len, Msg_component payload, void* data ) { cb_info_t* cbi; + if( (cbi = (cb_info_t *) data) == NULL ) { return; } + cbi->icount++; stats( *cbi ); + if( cbi->forward ) { // forward with no change to len or payload mbuf.Send_msg( Message::NO_CHANGE, NULL ); } } + int main( int argc, char** argv ) { std::unique_ptr x; char* port = (char *) "4560"; @@ -569,25 +585,30 @@ RMR Dump xAPP int ncb = 0; // number of callbacks registered int mtype; int nthreads = 1; + cbi = (cb_info_t *) malloc( sizeof( *cbi ) ); cbi->pcount = 0; cbi->icount = 0; cbi->stats_freq = 10; + ai = 1; // very simple flag parsing (no error/bounds checking) while( ai < argc ) { if( argv[ai][0] != '-' ) { // break on first non-flag break; } + // very simple arg parsing; each must be separate -x -y not -xy. switch( argv[ai][1] ) { case 'f': // enable packet forwarding cbi->forward = true; break; + case 'p': // define port port = argv[ai+1]; ai++; break; + case 's': // stats frequency cbi->stats_freq = atoi( argv[ai+1] ); if( cbi->stats_freq < 5 ) { // enforce sanity @@ -595,6 +616,7 @@ RMR Dump xAPP } ai++; break; + case 't': // thread count nthreads = atoi( argv[ai+1] ); if( nthreads < 1 ) { @@ -602,13 +624,16 @@ RMR Dump xAPP } ai++; break; + case 'v': // simple verbose bump cbi->vlevel++; break; + case 'V': // explicit verbose level cbi->vlevel = atoi( argv[ai+1] ); ai++; break; + default: fprintf( stderr, "unrecognised option: %s\\n", argv[ai] ); fprintf( stderr, "usage: %s [-f] [-p port] " @@ -620,12 +645,16 @@ RMR Dump xAPP "1 message info 2 payload dump\\n" ); exit( 1 ); } + ai++; } + cbi->hdr = cbi->stats_freq; fprintf( stderr, " listening on port: %s\\n", port ); + // create xapp, wait for route table if forwarding x = std::unique_ptr( new Xapp( port, cbi->forward ) ); + // register callback for each type on the command line while( ai < argc ) { mtype = atoi( argv[ai] ); @@ -634,130 +663,155 @@ RMR Dump xAPP x->Add_msg_cb( mtype, cb1, cbi ); ncb++; } + if( ncb < 1 ) { fprintf( stderr, " no message types specified on the command line\\n" ); exit( 1 ); } + x->Add_msg_cb( x->DEFAULT_CALLBACK, cbd, cbi ); // register default cb + fprintf( stderr, " starting driver\\n" ); x->Run( nthreads ); + // return from run() is not expected, but some compilers might // compilain if there isn't a return value here. return 0; } - - - Figure 8: Simple callback application. - -Callback Receiver --------------------------------------------------------------------------------------------- - - This sample programme implements a simple message listener - which registers three callback functions to process two - specific message types and a default callback to handle - unrecognised messages. - - When a message of type 1 is received, it will send two - response messages back to the sender. Two messages are sent - in order to illustrate that it is possible to send multiple - responses using the same received message. - - The programme illustrates how multiple listening threads can - be used, but the programme is **not** thread safe; to keep - this example as simple as possible, the counters are not - locked when incremented. - - - :: - + + Figure 8: Simple callback application. + + +Callback Receiver +----------------- + + This sample programme implements a simple message listener + which registers three callback functions to process two + specific message types and a default callback to handle + unrecognised messages. + + When a message of type 1 is received, it will send two + response messages back to the sender. Two messages are sent + in order to illustrate that it is possible to send multiple + responses using the same received message. + + The programme illustrates how multiple listening threads can + be used, but the programme is **not** thread safe; to keep + this example as simple as possible, the counters are not + locked when incremented. + + + :: + #include + #include "ricxfcpp/message.hpp" #include "ricxfcpp/msg_component.hpp" #include "ricxfcpp/xapp.hpp" + // counts; not thread safe long cb1_count = 0; long cb2_count = 0; long cbd_count = 0; + long cb1_lastts = 0; long cb1_lastc = 0; + // respond with 2 messages for each type 1 received void cb1( Message& mbuf, int mtype, int subid, int len, Msg_component payload, void* data ) { long now; long total_count; + // illustrate that we can use the same buffer for 2 rts calls mbuf.Send_response( 101, -1, 5, (unsigned char *) "OK1\\n" ); mbuf.Send_response( 101, -1, 5, (unsigned char *) "OK2\\n" ); + cb1_count++; } + // just count messages - void cb2( Message& mbuf, int mtype, int subid, int len, + void cb2( Message& mbuf, int mtype, int subid, int len, Msg_component payload, void* data ) { cb2_count++; } + // default to count all unrecognised messages - void cbd( Message& mbuf, int mtype, int subid, int len, + void cbd( Message& mbuf, int mtype, int subid, int len, Msg_component payload, void* data ) { cbd_count++; } + int main( int argc, char** argv ) { Xapp* x; char* port = (char *) "4560"; int ai = 1; // arg processing index int nthreads = 1; + // very simple flag processing (no bounds/error checking) while( ai < argc ) { if( argv[ai][0] != '-' ) { break; } + switch( argv[ai][1] ) { // we only support -x so -xy must be -x -y case 'p': port = argv[ai+1]; ai++; break; + case 't': nthreads = atoi( argv[ai+1] ); ai++; break; } + ai++; } + fprintf( stderr, " listening on port: %s\\n", port ); fprintf( stderr, " starting %d threads\\n", nthreads ); + x = new Xapp( port, true ); x->Add_msg_cb( 1, cb1, NULL ); // register callbacks x->Add_msg_cb( 2, cb2, NULL ); x->Add_msg_cb( x->DEFAULT_CALLBACK, cbd, NULL ); + x->Run( nthreads ); // let framework drive // control should not return } - - - Figure 9: Simple callback application. - - -Looping Sender --------------------------------------------------------------------------------------------- - - This is another very simple application which demonstrates - how an application can control its own listen loop while - sending messages. As with the other examples, some error - checking is skipped, and short cuts have been made in order - to keep the example small and to the point. - - - :: - + + Figure 9: Simple callback application. + + + +Looping Sender +-------------- + + This is another very simple application which demonstrates + how an application can control its own listen loop while + sending messages. As with the other examples, some error + checking is skipped, and short cuts have been made in order + to keep the example small and to the point. + + + :: + + #include #include #include + #include #include + #include "ricxfcpp/xapp.hpp" + extern int main( int argc, char** argv ) { std::unique_ptr xfw; std::unique_ptr msg; Msg_component payload; // special type of unique pointer to the payload + int sz; int len; int i; @@ -767,11 +821,14 @@ Looping Sender int mtype = 0; int rmtype; // received message type int delay = 1000000; // mu-sec delay; default 1s + + // very simple flag processing (no bounds/error checking) while( ai < argc ) { if( argv[ai][0] != '-' ) { break; } + // we only support -x so -xy must be -x -y switch( argv[ai][1] ) { // delay between messages (mu-sec) @@ -779,10 +836,12 @@ Looping Sender delay = atoi( argv[ai+1] ); ai++; break; + case 'p': port = argv[ai+1]; ai++; break; + // timeout in seconds; we need to convert to ms for rmr calls case 't': response_to = atoi( argv[ai+1] ) * 1000; @@ -791,25 +850,32 @@ Looping Sender } ai++; } + fprintf( stderr, " response timeout set to: %d\\n", response_to ); fprintf( stderr, " listening on port: %s\\n", port ); + // get an instance and wait for a route table to be loaded xfw = std::unique_ptr( new Xapp( port, true ) ); msg = xfw->Alloc_msg( 2048 ); + for( i = 0; i < 100; i++ ) { mtype++; if( mtype > 10 ) { mtype = 0; } + // we'll reuse a received message; get max size sz = msg->Get_available_size(); + // direct access to payload; add something silly payload = msg->Get_payload(); len = snprintf( (char *) payload.get(), sz, "This is message %d\\n", i ); + // payload updated in place, prevent copy by passing nil if ( ! msg->Send_msg( mtype, Message::NO_SUBID, len, NULL )) { fprintf( stderr, " send failed: %d\\n", i ); } + // receive anything that might come back msg = xfw->Receive( response_to ); if( msg != NULL ) { @@ -820,12 +886,12 @@ Looping Sender } else { msg = xfw->Alloc_msg( 2048 ); } + if( delay > 0 ) { usleep( delay ); } } } - - - Figure 10: Simple looping sender application. - + + Figure 10: Simple looping sender application. + diff --git a/ext/jsmn b/ext/jsmn new file mode 160000 index 0000000..7b6858a --- /dev/null +++ b/ext/jsmn @@ -0,0 +1 @@ +Subproject commit 7b6858a5855299d173c5ab2b46e611bf9961cbef diff --git a/src/json/CMakeLists.txt b/src/json/CMakeLists.txt new file mode 100644 index 0000000..ac6ff25 --- /dev/null +++ b/src/json/CMakeLists.txt @@ -0,0 +1,42 @@ +# vim: sw=4 ts=4 noet: +# +#================================================================================== +# Copyright (c) 2020 Nokia +# Copyright (c) 2020 AT&T Intellectual Property. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#================================================================================== +# + + +# For clarity: this generates object, not a lib as the CM command implies. +# +add_library( json_objects OBJECT + jwrapper.c + jhash.cpp +) + +target_include_directories ( json_objects PUBLIC + $ + + $ +) + +# header files should go into .../include/xfcpp/ +if( DEV_PKG ) + install( FILES + jhash.hpp + DESTINATION ${install_inc} + ) +endif() + diff --git a/src/json/README b/src/json/README new file mode 100644 index 0000000..6f775a3 --- /dev/null +++ b/src/json/README @@ -0,0 +1,11 @@ + +This directory contains the code to allow the framework to +provide a light-weight json parser. The parser, based on +a the third party Jasmn code (link below) parses a json +"blob" into a symbol table (rmr_symtab). The xcpp_json +class provides the API to then access the parsed data. + + +The jsmn package is included at the root as an submodule +and is used only at build time. + diff --git a/src/json/jhash.cpp b/src/json/jhash.cpp new file mode 100644 index 0000000..14a0486 --- /dev/null +++ b/src/json/jhash.cpp @@ -0,0 +1,283 @@ +// vi: ts=4 sw=4 noet: +/* +================================================================================== + Copyright (c) 2020 Nokia + Copyright (c) 2020 AT&T Intellectual Property. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +================================================================================== +*/ + +/* + Mnemonic: jhash.cpp + Abstract: This class provides the ability to parse a json string into + a hashtable, and exposes various functions that can be used + to read the data from the hash. + + Date: 26 June 2020 + Author: E. Scott Daniels +*/ + + +#include + +#include "jwrapper.h" +#include "jhash.hpp" + +// ------------------------------------------------------------------------ + + + +// ----------- construction/destruction housekeeping things ------------------- +/* + Accept a string that contains valid json. Causes it to be parsed + after which the functions provided by the class can be used to + suss out the values. +*/ +Jhash::Jhash( const char* jbuf ) : + st( jw_new( jbuf ) ), + master_st( NULL ) +{ /* empty body */ } + +//Jhash::Jhash( Jhash&& soi ); // mover +//Jhash::Jhash& operator=( Jhash&& soi ); // move operator + +/* + Blow it away. +*/ +Jhash::~Jhash() { + if( master_st != NULL ) { // revert blob set if needed + st = master_st; + } + + jw_nuke( st ); + st = NULL; + master_st = NULL; +} + + +// ------------ public API --------------------------------------------------- + +// IMPORTANT: all underlying jwrapper functions check for nil st and name pointers +// so that is NOT needed in this code. + + +// --- root control ---------------------------------------------------------- +/* + Sets the "root" to the named blob. Until unset, all subsequent + calls to Jhash functions (e.g. Is_missing()) will use this for the + look up. Returns true if name exists and was indeed a blob. + + It is legit to call set multiple times without unsetting; set + overlays with the named root; unset needs only to be called to + return to the top level. +*/ +bool Jhash::Set_blob( const char* name ) { + void* bst; // blob symbol table + + if( master_st == NULL ) { // must capture master + master_st == st; + } + + if( (bst = jw_blob( st, name )) != NULL ) { + st = bst; + return true; + } + + return false; +} + +/* + Return the suss root (blob root) to the root of the symtab. +*/ +void Jhash::Unset_blob( ) { + if( master_st != NULL ) { + st = master_st; + } +/* + else { +fprintf( stderr, "bad unset pointer%p %p\n", master_st, st ); +} +*/ +} + +// ---------------- debugging and sanity checks --------------------------------------- +/* + Returns true if there were parse errors which resulted in an empty + or non-existant hash. + + Right now we don't have much to work with other than checking for a + nil table. +*/ +bool Jhash::Parse_errors( ) { + return st == NULL; +} + +/* + Dump the selected blob as much as we can. +*/ +void Jhash::Dump() { + jw_dump( st ); +} + +// ---------------- type testing ----------------------------------------- + +/* + These funcitons return true if the named object in the current blob + is the indicated type +*/ +bool Jhash::Is_value( const char* name ) { + return jw_is_value( st, name ) == 1; +} + +bool Jhash::Is_bool( const char* name ) { + return jw_is_bool( st, name ) == 1; +} + +bool Jhash::Is_null( const char* name ) { + return jw_is_null( st, name ) == 1; +} + +bool Jhash::Is_string( const char* name ) { + return jw_is_string( st, name ) == 1; +} + +/* + These functions return true if the indicated element in the array + is the indicated type. +*/ +bool Jhash::Is_string_ele( const char* name, int eidx ) { + return jw_is_string_ele( st, name, eidx ) == 1; +} + +bool Jhash::Is_value_ele( const char* name, int eidx ) { + return jw_is_value_ele( st, name, eidx ) == 1; +} + +bool Jhash::Is_bool_ele( const char* name, int eidx ) { + return jw_is_bool_ele( st, name, eidx ) == 1; +} + +bool Jhash::Is_null_ele( const char* name, int eidx ) { + return jw_is_null_ele( st, name, eidx ) == 1; +} + + +// ---------------- presence ------------------------------------------------ +/* + Returns true if the named element is in the hash. +*/ +bool Jhash::Exists( const char* name ) { + return jw_exists( st, name ) == 1; +} + +/* + Returns true if the named element is not in the hash. +*/ +bool Jhash::Is_missing( const char* name ) { + return jw_missing( st, name ) == 1; +} + +// ---------------- value sussing ---------------------------------------- + +/* + Returns the boolean value for the object if it is a bool; false otherwise. + + Symtab saves bool values as 1 for true and doesn't provide a bool fetch + function. So, fetch the value and return true if it is 1. +*/ +bool Jhash::Bool( const char* name ) { + int v; + v = (int) jw_value( st, name ); + return v == 1; +} + +/* + Returns a C++ string to the named object; If the element is not + in the hash an empty string is returned. +*/ +std::string Jhash::String( const char* name ) { + std::string rv = ""; + char* hashv; + + if( (hashv = jw_string( st, name )) != NULL ) { + rv = std::string( hashv ); + } + + return rv; +} + + +/* + Returns the value assocated with the named object; If the element is not + in the hash 0 is returned. +*/ +double Jhash::Value( const char* name ) { + return jw_value( st, name ); +} + +// ------ array related things -------------------------------------------- + +/* + Return the length of the named array, or -1 if it doesn't exist. +*/ +int Jhash::Array_len( const char* name ) { + return jw_array_len( st, name ); +} + + +/* + Sets the blob in the array [eidx] to the current reference blob. +*/ +bool Jhash::Set_blob_ele( const char* name, int eidx ) { + void* bst; + + if( (bst = jw_obj_ele( st, name, eidx )) != NULL ) { + if( master_st == NULL ) { // must capture master + master_st = st; + } + + st = bst; + return true; + } + + return false; +} +/* + Return the string at index eidx in the array . +*/ +std::string Jhash::String_ele( const char* name, int eidx ) { + std::string rv = ""; + char* hashv; + + if( (hashv = jw_string_ele( st, name, eidx )) != NULL ) { + rv = std::string( hashv ); + } + + return rv; +} + +/* + Return the value at index eidx in the array . +*/ +double Jhash::Value_ele( const char* name, int eidx ) { + return jw_value_ele( st, name, eidx ); +} + +/* + Return the bool value at index eidx in the array . +*/ +bool Jhash::Bool_ele( const char* name, int eidx ) { + return jw_bool_ele( st, name, eidx ) == 1; +} + diff --git a/src/json/jhash.hpp b/src/json/jhash.hpp new file mode 100644 index 0000000..3ff6498 --- /dev/null +++ b/src/json/jhash.hpp @@ -0,0 +1,86 @@ +// vi: ts=4 sw=4 noet: +/* +================================================================================== + Copyright (c) 2020 Nokia + Copyright (c) 2020 AT&T Intellectual Property. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +================================================================================== +*/ + +/* + Mnemonic: jhash.hpp + Abstract: This class provides the ability to parse a json string into + a hashtable, and exposes various functions that can be used + to read the data from the hash. + + Date: 26 June 2020 + Author: E. Scott Daniels +*/ + +#ifndef _JHASH_HPP +#define _JHASH_HPP + + +#include + +// ------------------------------------------------------------------------ + +class Jhash { + private: + void* st; // the resulting symbol table generated by parse + void* master_st; // if user switches to a sub-blob; this tracks the original root st + + Jhash& operator=( const Jhash& soi ); // jhashes cannot be copied because of underlying symbol table goo + Jhash( const Jhash& soi ); + + public: + + Jhash( const char* jblob ); // builder + //Jhash( Message&& soi ); // mover + //Jhash& operator=( Message&& soi ); // move operator + ~Jhash(); // destruction + + bool Set_blob( const char* name ); // blob/root selection + void Unset_blob( ); + bool Set_blob_ele( const char* name, int eidx ); // set from an array element + + bool Parse_errors( ); + void Dump(); + + std::string String( const char* name ); // value fetching + double Value( const char* name ); + bool Bool( const char* name ); + + bool Exists( const char* name ); // presence checking + bool Is_missing( const char* name ); + + bool Is_bool( const char* name ); // type checking functions + bool Is_null( const char* name ); + bool Is_string( const char* name ); + bool Is_value( const char* name ); + + int Array_len( const char* name ); + + bool Is_bool_ele( const char* name, int eidx ); // type of array element checks + bool Is_null_ele( const char* name, int eidx ); + bool Is_string_ele( const char* name, int eidx ); + bool Is_value_ele( const char* name, int eidx ); + + bool Bool_ele( const char* name, int eidx ); // array oriented sussing functions + std::string String_ele( const char* name, int eidx ); + double Value_ele( const char* name, int eidx ); +}; + + +#endif diff --git a/src/json/jwrapper.c b/src/json/jwrapper.c new file mode 100644 index 0000000..f6c32c3 --- /dev/null +++ b/src/json/jwrapper.c @@ -0,0 +1,807 @@ + +// vi: ts=4 sw=4 noet: +/* +================================================================================== + Copyright (c) 2020 Nokia + Copyright (c) 2020 AT&T Intellectual Property. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +================================================================================== +*/ + +/* + Mnemonic: jwrapper.c + Abstract: A wrapper interface to the jsmn library which makes it a bit easier + to use. Parses a json string capturing the contents in a symtab. + + This code is based on the AT&T VFd open source library available + on github.com/att/vfd. The changes are mostly to port to the + RMR version of symtab from VFd's version. + + Author: E. Scott Daniels + Date: 26 June 2020 + +*/ + +#include +#include +#include +#include + +#ifndef DEBUG + #define DEBUG 0 +#endif + +#define JSMN_STATIC 1 // jsmn no longer builds into a library; this pulls as static functions +#include +//#include <../../ext/jsmn/jsmn.h> + +#include + +#define JSON_SYM_NAME "_jw_json_string" +#define MAX_THINGS 1024 * 4 // max objects/elements + +#define PT_UNKNOWN 0 // primative types; unknown for non prim +#define PT_VALUE 1 +#define PT_BOOL 2 +#define PT_NULL 3 +#define PT_STRING 4 + +#define OBJ_SPACE 1 // space in the symbol table where json bits are stashed + +extern void jw_nuke( void* st ); + +// --------------------------------------------------------------------------------------- + +/* + This is what we will manage in the symtab. Right now we store all values (primatives) + as double, but we could be smarter about it and look for a decimal. Unsigned and + differences between long, long long etc are tough. +*/ +typedef struct jthing { + int jsmn_type; // propigated type from jsmn (jsmn constants) + int prim_type; // finer grained primative type (bool, null, value) + int nele; // number of elements if applies + union { + double fv; + void *pv; + } v; +} jthing_t; + + +/* + Given the json token, 'extract' the element by marking the end with a + nil character, and returning a pointer to the start. We do this so that + we don't create a bunch of small buffers that must be found and freed; we + can just release the json string and we'll be done (read won't leak). +*/ +static char* extract( char* buf, jsmntok_t *jtoken ) { + buf[jtoken->end] = 0; + return &buf[jtoken->start]; +} + +/* + create a new jthing and add a reference to it in the symbol table st. + sets the number of elements to 1 by default. +*/ +static jthing_t *mk_thing( void *st, char *name, int jsmn_type ) { + jthing_t *jtp = NULL; + + if( st != NULL && + name != NULL && + (jtp = (jthing_t *) malloc( sizeof( *jtp ) )) != NULL ) { + + if( DEBUG ) { + fprintf( stderr, " jwrapper adding: %s type=%d\n", name, jsmn_type ); + } + + jtp->jsmn_type = jsmn_type; + jtp->prim_type = PT_UNKNOWN; // caller must set this + jtp->nele = 1; + jtp->v.fv = 0; + + rmr_sym_put( st, name, OBJ_SPACE, jtp ); + } else { + fprintf( stderr, "[WARN] jwrapper: unable to create '%s' type=%d\n", name, jsmn_type ); + } + + return jtp; +} + + +/* + Find the named array. Returns a pointer to the jthing that represents + the array (type, size and pointer to actual array of jthings). + Returns nil pointer if the named thing isn't there or isn't an array. +*/ +static jthing_t* suss_array( void* st, const char* name ) { + jthing_t* jtp = NULL; // thing that is referenced by the symtab + + if( st != NULL && + name != NULL && + (jtp = (jthing_t *) rmr_sym_get( st, name, OBJ_SPACE )) != NULL ) { + + jtp = jtp->jsmn_type == JSMN_ARRAY ? jtp : NULL; + } + + return jtp; +} + +/* + Suss an array from the hash and return the ith element. +*/ +static jthing_t* suss_element( void* st, const char* name, int idx ) { + jthing_t* jtp; // thing that is referenced by the symtab + jthing_t* jarray; + jthing_t* rv = NULL; + + if( (jtp = suss_array( st, name )) != NULL && // have pointer + idx >= 0 && // and in range + idx < jtp->nele ) { + + if( (jarray = jtp->v.pv) != NULL ) { + rv = &jarray[idx]; + } + } + + return rv; +} + + +/* + Invoked for each thing in the symtab; we free the things that actually point to + allocated data (e.g. arrays) and recurse to handle objects. +*/ +static void nix_things( void* st, void* se, const char* name, void* ele, void *data ) { + jthing_t* j; + jthing_t* jarray; + int i; + + j = (jthing_t *) ele; + if( j ) { + switch( j->jsmn_type ) { + case JSMN_ARRAY: + if( (jarray = (jthing_t *) j->v.pv) != NULL ) { + for( i = 0; i < j->nele; i++ ) { // must look for embedded objects + if( jarray[i].jsmn_type == JSMN_OBJECT ) { + jw_nuke( jarray[i].v.pv ); + jarray[i].jsmn_type = JSMN_UNDEFINED; // prevent accidents + } + } + + free( j->v.pv ); // must free the array (arrays aren't nested, so all things in the array don't reference allocated mem) + } + break; + + case JSMN_OBJECT: + jw_nuke( j->v.pv ); + j->jsmn_type = JSMN_UNDEFINED; // prevent a double free + break; + } + } +} + +/* + Invoked for each thing and prints what we can to stderr. +*/ +static void dump_things( void* st, void* se, const char* name, void* ele, void *data ) { + jthing_t* j; + jthing_t* jarray; + int i; + + j = (jthing_t *) ele; + if( j ) { + fprintf( stderr, " jwrapper: element '%s' has ptype %d, jsmn type %d\n", name, j->prim_type, j->jsmn_type ); + } else { + fprintf( stderr, " jwrapper: element has no data: '%s'\n", name ); + } +} + +/* + Real work for parsing an object ({...}) from the json. Called by jw_new() and + recurses to deal with sub-objects. +*/ +void* parse_jobject( void* st, char *json, char* prefix ) { + jthing_t *jtp; // json thing that we just created + int i; + int n; + char *name; // name in the json + char *data; // data string from the json + jthing_t* jarray; // array of jthings we'll coonstruct + int size; + int osize; + int njtokens; // tokens actually sussed out + jsmn_parser jp; // 'parser' object + jsmntok_t *jtokens; // pointer to tokens returned by the parser + char pname[1024]; // name with prefix + char wbuf[256]; // temp buf to build a working name in + char* dstr; // dup'd string + + jsmn_init( &jp ); // does this have a failure mode? + + jtokens = (jsmntok_t *) malloc( sizeof( *jtokens ) * MAX_THINGS ); + if( jtokens == NULL ) { + fprintf( stderr, "[CRI] jwrapper: cannot allocate tokens array\n" ); + return NULL; + } + + njtokens = jsmn_parse( &jp, json, strlen( json ), jtokens, MAX_THINGS ); + + if( jtokens[0].type != JSMN_OBJECT ) { // if it's not an object then we can't parse it. + fprintf( stderr, "[WARN] jwrapper: badly formed json; initial opening bracket ({) not detected\n" ); + return NULL; + } + + if( DEBUG ) { + for( i = 1; i < njtokens-1; i++ ) { + fprintf( stderr, " %4d: size=%d start=%d end=%d %s\n", i, jtokens[i].size, jtokens[i].start, jtokens[i].end, extract( json, &jtokens[i] ) ); + } + } + + for( i = 1; i < njtokens-1; i++ ) { // we'll silently skip the last token if it's "name" without a value + if( jtokens[i].type != JSMN_STRING ) { + fprintf( stderr, "[WARN] jwrapper: badly formed json [%d]; expected name (string) found type=%d %s\n", i, jtokens[i].type, extract( json, &jtokens[i] ) ); + rmr_sym_free( st ); + return NULL; + } + name = extract( json, &jtokens[i] ); + if( *prefix != 0 ) { + snprintf( pname, sizeof( pname ), "%s.%s", prefix, name ); + name = pname; + } + + size = jtokens[i].size; + + i++; // at the data token now + switch( jtokens[i].type ) { + case JSMN_OBJECT: // save object in two ways: as an object 'blob' and in the current symtab using name as a base (original) + dstr = strdup( extract( json, &jtokens[i] ) ); + snprintf( wbuf, sizeof( wbuf ), "%s_json", name ); // must stash the json string in the symtab for clean up during nuke + rmr_sym_put( st, wbuf, OBJ_SPACE, dstr ); + + parse_jobject( st, dstr, name ); // recurse to add the object as objectname.xxxx elements + + if( (jtp = mk_thing( st, name, jtokens[i].type )) != NULL && // create thing and reference it in current symtab + (jtp->v.pv = (void *) rmr_sym_alloc( 255 ) ) != NULL ) { // object is just a blob + + dstr = strdup( extract( json, &jtokens[i] ) ); + rmr_sym_put( jtp->v.pv, JSON_SYM_NAME, OBJ_SPACE, dstr ); // must stash json so it is freed during nuke() + parse_jobject( jtp->v.pv, dstr, "" ); // recurse acorss the string and build a new symtab + + size = jtokens[i].end; // done with them, we need to skip them + i++; + while( i < njtokens-1 && jtokens[i].end < size ) { + if( DEBUG ){ + fprintf( stderr, "\tskip: [%d] object element start=%d end=%d (%s)\n", + i, jtokens[i].start, jtokens[i].end, extract( json, &jtokens[i]) ); + } + i++; + } + + i--; // must allow loop to bump past the last + } + break; + + case JSMN_ARRAY: + size = jtokens[i].size; // size is burried here, and not with the name + jtp = mk_thing( st, name, jtokens[i].type ); + + i++; // skip first ele; it is the whole array string which I don't grock the need for, but it's their code... + if( jtp == NULL ) { + fprintf( stderr, "[WARN] jwrapper: memory alloc error processing element [%d] in json\n", i ); + rmr_sym_free( st ); + return NULL; + } + jarray = jtp->v.pv = (jsmntok_t *) malloc( sizeof( *jarray ) * size ); // allocate the array + memset( jarray, 0, sizeof( *jarray ) * size ); + jtp->nele = size; + + for( n = 0; n < size; n++ ) { // for each array element + jarray[n].prim_type = PT_UNKNOWN; // assume not primative type + switch( jtokens[i+n].type ) { + + case JSMN_OBJECT: + jarray[n].v.pv = (void *) rmr_sym_alloc( 255 ); + if( jarray[n].v.pv != NULL ) { + jarray[n].jsmn_type = JSMN_OBJECT; + parse_jobject( jarray[n].v.pv, extract( json, &jtokens[i+n] ), "" ); // recurse acorss the string and build a new symtab + osize = jtokens[i+n].end; // done with them, we need to skip them + i++; + while( i+n < njtokens-1 && jtokens[n+i].end < osize ) { + i++; + } + i--; // allow incr at loop end + } + break; + + case JSMN_ARRAY: + fprintf( stderr, "[WARN] jwrapper: [%d] array element %d is not valid type (array) is not string or primative\n", i, n ); + n += jtokens[i+n].size; // this should skip the nested array + break; + + case JSMN_STRING: + data = extract( json, &jtokens[i+n] ); + jarray[n].v.pv = (void *) data; + jarray[n].prim_type = PT_STRING; + jarray[n].jsmn_type = JSMN_STRING; + break; + + case JSMN_PRIMITIVE: + data = extract( json, &jtokens[i+n] ); + switch( *data ) { + case 'T': + case 't': + jarray[n].v.fv = 1; + jarray[n].prim_type = PT_BOOL; + break; + + case 'F': + case 'f': + jarray[n].prim_type = PT_BOOL; + jarray[n].v.fv = 0; + break; + + case 'N': // assume null, nil, or some variant + case 'n': + jarray[n].prim_type = PT_NULL; + jarray[n].v.fv = 0; + break; + + default: + jarray[n].prim_type = PT_VALUE; + jarray[n].v.fv = strtod( data, NULL ); // store all numerics as double + break; + } + + jarray[n].jsmn_type = JSMN_PRIMITIVE; + break; + + case JSMN_UNDEFINED: + // fallthrough + default: + fprintf( stderr, "[WARN] jwrapper: [%d] array element %d is not valid type (unknown=%d) is not string or primative\n", i, n, jtokens[i].type ); + rmr_sym_free( st ); + return NULL; + break; + } + } + + i += size - 1; // must allow loop to push to next + break; + + case JSMN_STRING: + data = extract( json, &jtokens[i] ); + if( (jtp = mk_thing( st, name, jtokens[i].type )) != NULL ) { + jtp->prim_type = PT_STRING; + jtp->v.pv = (void *) data; // just point into the large json string + } + break; + + case JSMN_PRIMITIVE: + data = extract( json, &jtokens[i] ); + if( (jtp = mk_thing( st, name, jtokens[i].type )) != NULL ) { + switch( *data ) { // assume T|t is true and F|f is false + case 'T': + case 't': + jtp->prim_type = PT_BOOL; + jtp->v.fv = 1; + break; + + case 'F': + case 'f': + jtp->prim_type = PT_BOOL; + jtp->v.fv = 0; + break; + + case 'N': // Null or some form of that + case 'n': + jtp->prim_type = PT_NULL; + jtp->v.fv = 0; + break; + + default: + jtp->prim_type = PT_VALUE; + jtp->v.fv = strtod( data, NULL ); // store all numerics as double + break; + } + } + break; + + default: + fprintf( stderr, "[WARN] jwrapper: element [%d] is undefined or of unknown type\n", i ); + break; + } + } + + free( jtokens ); + return st; +} + +// --------------- public functions ----------------------------------------------------------------- + +/* + Destroy all operating structures assocaited with the symtab pointer passed in. +*/ +extern void jw_nuke( void* st ) { + char* buf; // pointer to the original json to free + + if( st != NULL ) { + rmr_sym_foreach_class( st, OBJ_SPACE, nix_things, NULL ); // free anything that the symtab references + rmr_sym_free( st ); // free the symtab itself + } +} + +/* + Scan the given st and write some useful (?) info to stderr. +*/ +extern void jw_dump( void* st ) { + if( st != NULL ) { + rmr_sym_foreach_class( st, OBJ_SPACE, dump_things, NULL ); + } else { + fprintf( stderr, " jwrapper: dump: no table\n" ); + } +} + +/* + Given a json string, parse it, and put the things into a symtab. + return the symtab pointer to the caller. They pass the symtab + pointer back to the various get functions. + + This is the entry point. It sets up the symbol table and invokes the parse object + funtion to start at the first level. Parse object will recurse for nested objects + if present. +*/ +extern void* jw_new( const char* json ) { + void *st; // symbol table + char* djson; // dup so we can save it + void* rp = NULL; // return value + + if( json != NULL && (st = rmr_sym_alloc( MAX_THINGS )) != NULL ) { + djson = strdup( json ); // allows user to free/overlay their buffer as needed + rmr_sym_put( st, (unsigned char *) JSON_SYM_NAME, OBJ_SPACE, djson ); // must have a reference to the string until symtab is trashed + + rp = parse_jobject( st, djson, "" ); // empty prefix for the root object + } + + return rp; +} + +/* + Returns true (1) if the named field is missing. +*/ +extern int jw_missing( void* st, const char* name ) { + int rv = 0; + + if( st != NULL && name != NULL ) { + rv = rmr_sym_get( st, name, OBJ_SPACE ) == NULL; + } + + return rv; +} + +/* + Returns true (1) if the named field is in the blob; +*/ +extern int jw_exists( void* st, const char* name ) { + int rv = 0; + + if( st != NULL && name != NULL ) { + rv = rmr_sym_get( st, name, OBJ_SPACE ) != NULL; + } + + return rv; +} + +/* + Returns true (1) if the primative type is value (double). +*/ +extern int jw_is_value( void* st, const char* name ) { + jthing_t* jtp; // thing that is referenced by the symtab + int rv = 0; + + if( st != NULL && + name != NULL && + (jtp = (jthing_t *) rmr_sym_get( st, name, OBJ_SPACE )) != NULL ) { + + rv = jtp->prim_type == PT_VALUE; + } + + return rv; +} +/* + Returns true (1) if the primative type is string. +*/ +extern int jw_is_string( void* st, const char* name ) { + jthing_t* jtp; // thing that is referenced by the symtab + int rv = 0; + + if( st != NULL && + name != NULL && + (jtp = (jthing_t *) rmr_sym_get( st, name, OBJ_SPACE )) != NULL ) { + + rv = jtp->prim_type == PT_STRING; + } + + return rv; +} + +/* + Returns true (1) if the primative type is boolean. +*/ +extern int jw_is_bool( void* st, const char* name ) { + jthing_t* jtp; // thing that is referenced by the symtab + int rv = 0; + + if( st != NULL && + name != NULL && + (jtp = (jthing_t *) rmr_sym_get( st, name, OBJ_SPACE )) != NULL ) { + + rv = jtp->prim_type == PT_BOOL; + } + + return rv; +} + +/* + Returns true (1) if the primative type was a 'null' type. +*/ +extern int jw_is_null( void* st, const char* name ) { + jthing_t* jtp; // thing that is referenced by the symtab + int rv = 0; + + if( st != NULL && + name != NULL && + (jtp = (jthing_t *) rmr_sym_get( st, name, OBJ_SPACE )) != NULL ) { + + rv = jtp->prim_type == PT_NULL; + } + + return rv; +} + +/* + Look up the name in the symtab and return the string (data). +*/ +extern char* jw_string( void* st, const char* name ) { + jthing_t* jtp; // thing that is referenced by the symtab + char* rv = NULL; + + if( st != NULL && + name != NULL && + (jtp = (jthing_t *) rmr_sym_get( st, name, OBJ_SPACE )) != NULL ) { + + if( jtp->jsmn_type == JSMN_STRING ) { + rv = (char *) jtp->v.pv; + } + } + + return rv; +} + +/* + Look up name and return the value. +*/ +extern double jw_value( void* st, const char* name ) { + jthing_t* jtp; // thing that is referenced by the symtab + double rv = 0.0; + + if( st != NULL && + name != NULL && + (jtp = (jthing_t *) rmr_sym_get( st, name, OBJ_SPACE )) != NULL ) { + + if( jtp->jsmn_type == JSMN_PRIMITIVE ) { + rv = jtp->v.fv; + } + } + + return rv; +} + +/* + Look up name and return the blob (symtab). +*/ +extern void* jw_blob( void* st, const char* name ) { + jthing_t* jtp; // thing that is referenced by the symtab + void* rv = NULL; + + if( st != NULL && + name != NULL && + (jtp = (jthing_t *) rmr_sym_get( st, name, OBJ_SPACE )) != NULL ) { + + if( jtp->jsmn_type == JSMN_OBJECT ) { + rv = (void *) jtp->v.pv; + } + } + + return rv; +} + +/* + Look up the element and return boolean state; This takes the C approach and + returns true/false based on the value. +*/ +extern int jw_bool_ele( void* st, const char* name, int idx ) { + jthing_t* jtp; // thing that is referenced by the symtab entry + int rv = 0; + + if( st != NULL && + name != NULL && + (jtp = suss_element( st, name, idx )) != NULL ) { + + rv = !! ((int) jtp->v.fv); + } + + return rv; +} +/* + Look up array element as a string. Returns NULL if: + name is not an array + name is not in the hash + index is out of range + element is not a string +*/ +extern char* jw_string_ele( void* st, const char* name, int idx ) { + jthing_t* jtp; // thing that is referenced by the symtab entry + char* rv = NULL; + + if( st != NULL && + name != NULL && + (jtp = suss_element( st, name, idx )) != NULL ) { + + if( jtp->jsmn_type == JSMN_STRING ) { + rv = (char *) jtp->v.pv; + } + } + + return rv; +} + +/* + Look up array element as a value. Returns 0 if: + name is not an array + name is not in the hash + index is out of range + element is not a value +*/ +extern double jw_value_ele( void* st, const char* name, int idx ) { + jthing_t* jtp; // thing that is referenced by the symtab entry + double rv = 0.0; + + if( st != NULL && + name != NULL && + (jtp = suss_element( st, name, idx )) != NULL ) { + + if( jtp->prim_type == PT_VALUE ) { + rv = jtp->v.fv; + } + } + + return rv; +} +/* + Look up the element and check to see if it is a string. + Return true (1) if it is. +*/ +extern int jw_is_string_ele( void* st, const char* name, int idx ) { + jthing_t* jtp; // thing that is referenced by the symtab entry + int rv = 0; + + if( st != NULL && + name != NULL && + (jtp = suss_element( st, name, idx )) != NULL ) { + + rv = jtp->prim_type == PT_STRING; + } + + return rv; +} + +/* + Look up the element and check to see if it is a value primative. + Return true (1) if it is. +*/ +extern int jw_is_value_ele( void* st, const char* name, int idx ) { + jthing_t* jtp; // thing that is referenced by the symtab entry + int rv = 0; + + if( st != NULL && + name != NULL && + (jtp = suss_element( st, name, idx )) != NULL ) { + + rv = jtp->prim_type == PT_VALUE; + } + + return rv; +} + +/* + Look up the element and check to see if it is a boolean primative. + Return true (1) if it is. +*/ +extern int jw_is_bool_ele( void* st, const char* name, int idx ) { + jthing_t* jtp; // thing that is referenced by the symtab entry + int rv = 0; + + if( st != NULL && + name != NULL && + (jtp = suss_element( st, name, idx )) != NULL ) { + + rv = jtp->prim_type == PT_BOOL; + } + + return rv; +} + +/* + Look up the element and check to see if it is a null primative. + Return true (1) if it is. +*/ +extern int jw_is_null_ele( void* st, const char* name, int idx ) { + jthing_t* jtp; // thing that is referenced by the symtab entry + int rv = 0; + + if( st != NULL && + name != NULL && + (jtp = suss_element( st, name, idx )) != NULL ) { + + rv = jtp->prim_type == PT_NULL; + } + + return rv; +} + +/* + Look up array element as an object. Returns NULL if: + name is not an array + name is not in the hash + index is out of range + element is not an object + + An object in an array is a standalone symbol table. Thus the object + is treated differently than a nested object whose members are a + part of the parent namespace. An object in an array has its own + namespace. +*/ +extern void* jw_obj_ele( void* st, const char* name, int idx ) { + jthing_t* jtp; // thing that is referenced by the symtab entry + void* rv = NULL; + + if( st != NULL && + name != NULL && + (jtp = suss_element( st, name, idx )) != NULL ) { + + if( jtp->jsmn_type == JSMN_OBJECT ) { + rv = (void *) jtp->v.pv; + } + } + + return rv; +} + +/* + Return the size of the array named. Returns -1 if the thing isn't an array, + and returns the number of elements otherwise. +*/ +extern int jw_array_len( void* st, const char* name ) { + jthing_t* jtp; // thing that is referenced by the symtab entry + int rv = -1; + + if( st != NULL && + name != NULL && + (jtp = suss_array( st, name )) != NULL ) { + + rv = jtp->nele; + } + + return rv; +} diff --git a/src/json/jwrapper.h b/src/json/jwrapper.h new file mode 100644 index 0000000..c560dbc --- /dev/null +++ b/src/json/jwrapper.h @@ -0,0 +1,76 @@ +// vi: ts=4 sw=4 noet: +/* +================================================================================== + Copyright (c) 2020 Nokia + Copyright (c) 2020 AT&T Intellectual Property. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +================================================================================== +*/ + +/* + Mnemonic: jwrapper.h + Abstract: Header file for jwrapper; mostly prototypes. + + Date: 26 June 2020 + Author: E. Scott Daniels +*/ + + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef _JWRAPPER_H +#define _JWRAPPER_H + + +//---------------- jwrapper ------------------------------------------------------------------------------- +extern void jw_nuke( void* st ); +extern void* jw_new( const char* json ); + +extern int jw_bool_ele( void* st, const char* name, int idx ); +extern void jw_dump( void* st ); +extern int jw_missing( void* st, const char* name ); +extern int jw_exists( void* st, const char* name ); +extern char* jw_string( void* st, const char* name ); +extern double jw_value( void* st, const char* name ); +extern void* jw_blob( void* st, const char* name ); +extern char* jw_string_ele( void* st, const char* name, int idx ); +extern double jw_value_ele( void* st, const char* name, int idx ); +extern void* jw_obj_ele( void* st, const char* name, int idx ); +extern int jw_array_len( void* st, const char* name ); + +extern int jw_is_string( void* st, const char* name ); +extern int jw_is_value( void* st, const char* name ); +extern int jw_is_bool( void* st, const char* name ); +extern int jw_is_null( void* st, const char* name ); + +extern int jw_is_string_ele( void* st, const char* name, int idx ); +extern int jw_is_value_ele( void* st, const char* name, int idx ); +extern int jw_is_bool_ele( void* st, const char* name, int idx ); +extern int jw_is_null_ele( void* st, const char* name, int idx ); + +// ---------------- jw_xapi --------------------------------------------------------------------------------- +extern int jwx_get_bool( void* jblob, char const* field_name, int def_value ); +extern double jwx_get_value( void* jblob, char const* field_name, double def_value ); +extern int jwx_get_ivalue( void* jblob, char const* field_name, int def_value ); +extern char* jwx_get_value_as_str( void* jblob, char const* field_name, char const* def_value, int fmt ); +extern char* jwx_get_str( void* jblob, char const* field_name, char const* def_value ); + +#endif + +#ifdef __cplusplus +} +#endif + diff --git a/test/Makefile b/test/Makefile index fcbf361..0abcc17 100644 --- a/test/Makefile +++ b/test/Makefile @@ -1,7 +1,9 @@ coverage_opts = -ftest-coverage -fprofile-arcs -binaries = unit_test +binaries = unit_test + +tests:: unit_test jhash_test # RMR emulation rmr_em.o:: rmr_em.c @@ -11,6 +13,14 @@ unit_test:: unit_test.cpp rmr_em.o # do NOT link the xapp lib; we include all modules in the test programme g++ -g $(coverage_opts) -I ../src/messaging unit_test.cpp -o unit_test rmr_em.o -lpthread +# build a special jwrapper object with coverage settings +jwrapper_test.o:: ../src/json/jwrapper.c ../src/json/jwrapper.h + cc $(coverage_opts) -I ../src/json -I ../ext/jsmn ../src/json/jwrapper.c -c -o jwrapper_test.o + +jhash_test:: jwrapper_test.o jhash_test.cpp + # do NOT link the xapp lib; we include all modules in the test programme + g++ -g $(coverage_opts) -I ../src/json -I ../ext/jsmn jhash_test.cpp -o jhash_test jwrapper_test.o -lrmr_si -lpthread + # prune gcov files generated by system include files clean:: rm -f *.h.gcov *.c.gcov diff --git a/test/jhash_test.cpp b/test/jhash_test.cpp new file mode 100644 index 0000000..be4171c --- /dev/null +++ b/test/jhash_test.cpp @@ -0,0 +1,268 @@ +// vim: ts=4 sw=4 noet : +/* +================================================================================== + Copyright (c) 2020 Nokia + Copyright (c) 2020 AT&T Intellectual Property. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +================================================================================== +*/ + +/* + Mnemonic: json_test.cpp + Abstract: Unit test for the json module. This expects that a static json + file exist in the current directory with a known set of fields, + arrays and objects that can be sussed out after parsing. The + expected file is test.json. + + Date: 26 June 2020 + Author: E. Scott Daniels +*/ + +#include +#include +#include +#include +#include +#include + +#include +#include + + +/* + Very simple file reader. Reads up to 8k into a single buffer and + returns the buffer as char*. Easier to put json test things in + a file than strings. +*/ +static char* read_jstring( char* fname ) { + char* rbuf; + int fd; + int len; + + rbuf = (char *) malloc( sizeof( char ) * 8192 ); + fd = open( fname, O_RDONLY, 0 ); + if( fd < 0 ) { + fprintf( stderr, " can't open test file: %s: %s\n", fname, strerror( errno ) ); + exit( 1 ); + } + + len = read( fd, rbuf, 8190 ); + if( len < 0 ) { + close( fd ); + fprintf( stderr, " read from file failed: %s: %s\n", fname, strerror( errno ) ); + exit( 1 ); + } + + rbuf[len] = 0; + close( fd ); + + return rbuf; +} + +// this also tests jwrapper.c but that is built as a special object to link in +// rather than including here. +// +#include "../src/json/jhash.hpp" +#include "../src/json/jhash.cpp" + +#include "ut_support.cpp" + +int main( int argc, char** argv ) { + int errors = 0; + Jhash* jh; + char* jstr; + std::string sval; + double val; + bool state; + int i; + int len; + int true_count = 0; + + set_test_name( "jhash_test" ); + jstr = read_jstring( (char *) "test.json" ); // read and parse the json + + fprintf( stderr, "read: (%s)\n", jstr ); + + jh = new Jhash( jstr ); + + if( jh == NULL ) { + fprintf( stderr, " could not parse json string from: test.json\n" ); + exit( 1 ); + } + + + sval = jh->String( (char *) "meeting_day" ); + fprintf( stderr, " sval=(%s)\n", sval.c_str() ); + errors += fail_if( sval.compare( "" ) == 0, "did not get meeting day string" ); + errors += fail_if( sval.compare( "Tuesday" ) != 0, "meeting day was not expected string" ); + + sval = jh->String( (char *) "meeting_place" ); + fprintf( stderr, " sval=(%s)\n", sval.c_str() ); + errors += fail_if( sval.compare( "" ) == 0, "did not get meeting place" ); + errors += fail_if( sval.compare( "16801 East Green Drive" ) != 0, "meeting place stirng was not correct" ); + + state = jh->Exists( (char *) "meeting_place" ); + errors += fail_if( !state, "test for meeting place exists did not return true" ); + + state = jh->Exists( (char *) "no-name" ); + errors += fail_if( state, "test for non-existant thing returned true" ); + + state = jh->Is_missing( (char *) "no-name" ); + errors += fail_if( !state, "missing test for non-existant thing returned false" ); + + state = jh->Is_missing( (char *) "meeting_place" ); + errors += fail_if( state, "missing test for existing thing returned true" ); + + val = jh->Value( (char *) "lodge_number" ); + errors += fail_if( val != 41.0, "lodge number value was not correct" ); + + val = jh->Value( (char *) "monthly_dues" ); + fprintf( stderr, " got dues: %.2f\n", val ); + errors += fail_if( val != (double) 43.5, "lodge dues value was not correct" ); + + len = jh->Array_len( (char *) "members" ); + fprintf( stderr, " got %d members\n", len ); + errors += fail_if( len != 4, "array length was not correct" ); + if( len > 0 ) { + for( i = 0; i < len; i++ ) { + if( ! jh->Set_blob_ele( (char *) "members", i ) ) { + errors++; + fprintf( stderr, (char *) "couldn't set blob for element %d\n", i ); + } else { + fprintf( stderr, (char *) " testing element %d of %d\n", i, len ); + + state = jh->Is_value( (char *) "age" ); + errors += fail_if( !state, "is value test for age returned false" ); + state = jh->Is_value( (char *) "married" ); + errors += fail_if( state, "is value test for married returned true" ); + + state = jh->Is_string( (char *) "occupation" ); + errors += fail_if( !state, "is string test for spouse returned false" ); + state = jh->Is_string( (char *) "married" ); + errors += fail_if( state, "is string test for married returned true" ); + + state = jh->Is_bool( (char *) "married" ); + errors += fail_if( !state, "is bool test for married returned false" ); + state = jh->Is_bool( (char *) "occupation" ); + errors += fail_if( state, "is bool test for spouse returned true" ); + + val = jh->Value( (char *) "age" ); + fprintf( stderr, " got age: %.2f\n", (double) val ); + errors += fail_if( val < 0, "age value wasn't positive" ); + + sval = jh->String( (char *) "name" ); + fprintf( stderr, " sval=(%s)\n", sval.c_str() ); + errors += fail_if( sval.compare( "" ) == 0, "no name found in element" ); + + if( jh->Bool( (char *) "married" ) ) { + true_count++; + } + } + + jh->Unset_blob(); // must return to root + } + + fprintf( stderr, " true count = %d\n", true_count ); + errors += fail_if( true_count != 3, "married == true count was not right" ); + } + + state = jh->Set_blob( (char *) "no-such-thing" ); + errors += fail_if( state, "setting blob to non-existant blob returned true" ); + + state = jh->Set_blob( (char *) "grand_poobah" ); + errors += fail_if( !state, "setting blob to existing blob failed" ); + if( state ) { + sval = jh->String( (char *) "elected" ); + fprintf( stderr, " sval=(%s)\n", sval.c_str() ); + errors += fail_if( sval != "February 2019", "blob 'elected' didn't return the expected string" ); + + state = jh->Exists( (char *) "monthly_dues" ); + errors += fail_if( state, "blob that shouldn't have a field reports it does" ); + + jh->Unset_blob( ); // ensure that this is found once we unset to root + state = jh->Exists( (char *) "monthly_dues" ); + errors += fail_if( !state, "after rest, root blob, that should have a field, reports it does not" ); + } + + + // ---- test array element value type checks ------------------------------------------------- + state = jh->Is_string_ele( (char *) "sponser", 1 ); + errors += fail_if( !state, "string element check on sponser failed" ); + state = jh->Is_string_ele( (char *) "current_on_dues", 1 ); + errors += fail_if( state, "string element check on non-stirng element returned true" ); + + state = jh->Is_value_ele( (char *) "dues_assistance", 1 ); + errors += fail_if( !state, "string element check on sponser failed" ); + state = jh->Is_value_ele( (char *) "current_on_dues", 1 ); + errors += fail_if( state, "string element check on non-stirng element returned true" ); + + state = jh->Is_bool_ele( (char *) "current_on_dues", 1 ); + errors += fail_if( !state, "string element check on sponser failed" ); + state = jh->Is_bool_ele( (char *) "sponser", 1 ); + errors += fail_if( state, "string element check on non-stirng element returned true" ); + + state = jh->Is_null( (char *) "nvt" ); + errors += fail_if( !state, "test for nil value returned false" ); + state = jh->Is_null( (char *) "lodge_number" ); + errors += fail_if( state, "nil test for non-nil value returned true" ); + + state = jh->Is_null_ele( (char *) "nvat", 0 ); + errors += fail_if( !state, "test for nil array element value returned false" ); + + + // ---- test sussing of elements from arrays ------------------------------------------------- + sval = jh->String_ele( (char *) "sponser", 1 ); + errors += fail_if( sval.compare( "" ) == 0, "get string element failed for sponser" ); + + val = jh->Value_ele( (char *) "dues_assistance", 1 ); + errors += fail_if( val == 0.0, "get value element for dues_assistance was zero" ); + + state = jh->Bool_ele( (char *) "current_on_dues", 1 ); + errors += fail_if( state, "bool ele test returned true for a false value" ); + state = jh->Bool_ele( (char *) "current_on_dues", 0 ); + errors += fail_if( !state, "bool ele test returned false for a true value" ); + + + val = jh->Value( (char *) "timestamp" ); + fprintf( stderr, " timestamp: %.10f\n", val ); + + + + + + delete jh; + + fprintf( stderr, " testing for failures; jwrapper error and warning messages expected\n" ); + // ---- these shouild all fail to parse, generate warnings to stderr, and drive error handling coverage ---- + jh = new Jhash( (char *) "{ \"bad\": [ [ 1, 2, 3 ], [ 3, 4, 5]] }" ); // drive the exception process for bad json + delete jh; + + jh = new Jhash( (char *) " \"bad\": 5 }" ); // no opening brace + state = jh->Parse_errors(); + errors += fail_if( !state, "parse errors check returned false when known errors exist" ); + delete jh; + + jh = new Jhash( (char *) "{ \"bad\": fred }" ); // no quotes + delete jh; + + jh = new Jhash( (char *) "{ \"bad: 456, \"good\": 100 }" ); // missing quote; impossible to detect error + jh->Dump(); // but dump should provide details + fprintf( stderr, " good value=%d\n", (int) val ); + delete jh; + + + // ---------------------------- end housekeeping --------------------------- + announce_results( errors ); + return !!errors; +} diff --git a/test/scrub_gcov.sh b/test/scrub_gcov.sh index 349fca2..51f0815 100755 --- a/test/scrub_gcov.sh +++ b/test/scrub_gcov.sh @@ -44,7 +44,6 @@ function mk_list { done } - list=$( mk_list ) if [[ -n $list ]] then diff --git a/test/test.json b/test/test.json new file mode 100644 index 0000000..6fd3ad1 --- /dev/null +++ b/test/test.json @@ -0,0 +1,50 @@ +{ + "lodge_number": 41, + "meeting_day": "Tuesday", + "meeting_place": "16801 East Green Drive", + "monthly_dues": 43.50, + + "grand_poobah": { + "name": "Larry K. Slate", + "elected": "February 2019", + "term": "2 years" + }, + + "members": [ + { + "name" : "Fred Flintstone", + "occupation" : "operator", + "age" : 45, + "married" : true, + "spouse" : "Wilma Flintsone" + }, + { + "name" : "Barney Rubble", + "occupation" : "sidekick", + "age" : 43, + "married" : true, + "spouse" : "Betty Rubble" + }, + { + "name" : "Larry K. Slate", + "occupation" : "manager", + "age" : 52, + "married" : true, + "spouse" : "Linda Slate" + }, + { + "name" : "Benjamin Gravel", + "occupation" : "sculpter", + "age" : 25, + "married" : false + } + ], + + "timestamp": 1593541361.450337059, + "comment": "these are to drive array building in jwrapper", + "current_on_dues": [ true, false, true, true ], + "dues_assistance": [ 0.2, 0.1, 0.25, 0.2 ], + "sponser": [ "slate", "slate", "stonewall", "brick" ], + "nvat": [ null, null ], + "nvt": null +} diff --git a/test/unit_test.sh b/test/unit_test.sh index 7144f9c..2f30686 100755 --- a/test/unit_test.sh +++ b/test/unit_test.sh @@ -30,14 +30,14 @@ # Make a list of our modules under test so that we don't look at gcov # files that are generated for system lib headers in /usr/* -# (bash makes the process of building a list of names harder than it +# (bash makes the process of building a list of names harder than it # needs to be, so use caution with the printf() call.) # function mk_list { grep -l "Source:\.\./src" *.gcov | while read f do printf "$f " # do NOT use echo or add \n! - done + done } function abort_if_error { @@ -70,18 +70,30 @@ do shift done +export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH +export LIBRARY_PATH=/usr/local/lib:$LIBRARY_PATH + make nuke >/dev/null -make unit_test >/tmp/PID$$.log 2>&1 +make tests >/tmp/PID$$.log 2>&1 abort_if_error $? "unable to make" +echo "tests successfully built" >&2 spew="cat" -./unit_test >/tmp/PID$$.log 2>&1 -abort_if_error $? "unit test failed" -gcov unit_test >/tmp/PID$$.gcov_log 2>&1 # suss out our gcov files +for x in unit_test jhash_test +do + ./$x >/tmp/PID$$.log 2>&1 + abort_if_error $? "test failed: $x" + gcov $x.c >/dev/null 2>&1 +done + +# wrapper_test is driven by jhash_test, but must be covered explicitly +gcov jwrapper_test.c >/dev/null 2>&1 + ./scrub_gcov.sh # remove cruft list=$( mk_list ) +echo "" echo "[INFO] coverage stats, discounted (raw), for the various modules:" ./parse_gcov.sh $list # generate simple, short, coverage stats diff --git a/test/ut_support.cpp b/test/ut_support.cpp new file mode 100644 index 0000000..8e6be90 --- /dev/null +++ b/test/ut_support.cpp @@ -0,0 +1,69 @@ +// vi: ts=4 sw=4 noet: +/* +================================================================================== + Copyright (c) 2020 AT&T Intellectual Property. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +================================================================================== +*/ + +/* + Mnemonic: ut_support.c + Abstract: Tools to make unit testing easier. + + Date: 16 June 2020 + Author: E. Scott Daniels +*/ + +#include + +static std::string test_name = "unknown"; + +/* + Set the name of the current tester +*/ +extern void set_test_name( std::string name ) { + test_name = name; +} + +/* + Returns 1 if the condition is true (not zero) +*/ +extern int fail_if( int cond, std::string reason ) { + if( cond ) { + fprintf( stderr, " %s: %s\n", test_name.c_str(), reason.c_str() ); + return 1; + } + + return 0; +} + +/* + Returns 1 if the condition is false. +*/ +extern int fail_if_false( int cond, std::string reason ) { + if( !cond ) { + fprintf( stderr, " %s: %s\n", test_name.c_str(), reason.c_str() ); + return 1; + } + + return 0; +} + +extern void announce_results( int errors ) { + if( errors > 0 ) { + fprintf( stderr, " %s: failed with %d errors\n", test_name.c_str(), errors ); + } else { + fprintf( stderr, " %s: passed with\n", test_name.c_str() ); + } +} -- 2.16.6