Thursday, 16 August 2018

VBA - JSON - REST APIs - Atomic vs Document-Driven

Very interesting article, Replace RESTful APIs with JSON-Pure saying that REST Apis as strictly described by Roy Fielding are probably not a good idea. Definitely worth a read and has influenced my design.

I want to write a web service that takes information scanned for a VBA project and uploads it to a web server for analysis. Initially, I had thought I'd make a network call for each method I'd find but then that adds up to tons of ntwork calls, also I'd have to expose my URLs to the user and finally I'd have to force fit my data to a REST url paradigm which I found quite challenging. This initial approach could be described as an atomic approach making many individual network calls. An alternative approach is to build up a whole document and post this document all in one network call.

In case you're interested the code that was used as the subject of the experiment is available here, Creating an SVG file with VBA .

So the following is a program that will use the Microsoft Visual Basic for Applications Extensibility 5.3 (VBIDE) library to scan a VBA project attached a workbook for classes and modules and procedures found therein.

The code uses simple string concatenation to build the JSON document but still uses the ScriptControl to debug malformed JSON.

The resultant JSON document is found below as well as the list of atomic calls for comparison. I'm opting for document driven approach for now on.


Option Explicit
Option Private Module

'* Tools->References
' MSScriptControl  Microsoft Script Control 1.0                        C:\Windows\SysWow64\msscript.ocx
' MSXML2           Microsoft XML, v6.0                                 C:\Windows\SysWOW64\msxml6.dll
' Scripting        Microsoft Scripting Runtime                         C:\Windows\SysWOW64\scrrun.dll
' VBIDE            Microsoft Visual Basic for App's Extensibility 5.3  C:\Program Files (x86)\Common Files\Microsoft Shared\VBA\VBA6\VBE6EXT.OLB

'******************************************************************************
'* This block implements the debug boolean (plus a default)
Private Const mbDEBUGJSONDEFAULT As Boolean = True
Private mvDebugJSON As Variant
Private Property Let DebugJSON(ByVal bRHS As Boolean)
    mvDebugJSON = bRHS
End Property
Private Property Get DebugJSON() As Boolean
    If IsEmpty(mvDebugJSON) Then mvDebugJSON = mbDEBUGJSONDEFAULT
    DebugJSON = mvDebugJSON
End Property
'******************************************************************************


Private Sub TestConvertArtefactsToModellerJSON()
    '*
    '* Main entry point, tests the code and shows the JSON parsed into an object
    '* of course one does not have to parse it here, one can just pass JSON string on to a webservice
    '*
    
    Dim wb As Excel.Workbook
    Set wb = Workbooks.Item("SVG.xlsm") '<--- put your own workbook in here!
    
    DebugJSON = False  '* this controls interim parsing, useful for debugging the document's components
    
    Dim sJSON As String
    sJSON = ReadComponentsWithVBIDE(wb)

    Debug.Print sJSON

    Dim objParsed As Object
    Set objParsed = SC.Run("JSON_parse", sJSON)
    
    Stop '* inspect objParsed in Locals Window
    
End Sub

Private Function ReadComponentsWithVBIDE(ByVal wb As Excel.Workbook) As String
    '*
    '* This function scans through all the classes and modules and reports on procedures found therein
    '*

    If wb Is Nothing Then Err.Raise vbObjectError, , "#Null wb!"
    
    Dim vbp As VBIDE.VBProject
    Set vbp = wb.VBProject
    
    Dim sJSON As String
    sJSON = VBA.Replace("{'projectName':'%foo%'", "%foo%", vbp.Name)
    
    Dim sJSONClasses As String
    sJSONClasses = "["
    
    Dim sJSONModules As String
    sJSONModules = "["
    
    Dim vbcLoop As VBIDE.VBComponent
    For Each vbcLoop In vbp.VBComponents
        
        If vbcLoop.Type = vbext_ct_ClassModule Or vbcLoop.Type = vbext_ct_StdModule Then
        
            Dim sJSONComponent As String
            sJSONComponent = ScanComponent(vbcLoop)
            
            Select Case vbcLoop.Type
            
            Case vbext_ct_ClassModule:
                sJSONClasses = sJSONClasses & VBA.IIf(Len(sJSONClasses) > 1, ",", "") & sJSONComponent
                
            Case vbext_ct_StdModule:
                sJSONModules = sJSONModules & VBA.IIf(Len(sJSONModules) > 1, ",", "") & sJSONComponent
            End Select

        End If
    Next
    
    sJSONClasses = sJSONClasses & "]"
    sJSONModules = sJSONModules & "]"
    
    Call ParseAndStringify(sJSONClasses)
    Call ParseAndStringify(sJSONModules)
    
    sJSON = sJSON & ",'classes':" & sJSONClasses
    sJSON = sJSON & ",'modules':" & sJSONModules
    sJSON = sJSON & "}"
        
    ReadComponentsWithVBIDE = ParseAndStringify(sJSON)
    
End Function

Private Function ScanComponent(ByVal vbc As VBIDE.VBComponent) As String
    '*
    '* This function will scan the source code of a component and report on any procedures found therein
    '*
    Dim dicProcs As Scripting.Dictionary
    Set dicProcs = New Scripting.Dictionary
    
    Dim dotnetlistProcs As Object
    Set dotnetlistProcs = VBA.CreateObject("System.Collections.ArrayList") '* need for sorting
    
    Dim cm As VBIDE.CodeModule
    Set cm = vbc.CodeModule
    
    Dim lLineLoop As Long
    For lLineLoop = 1 To cm.CountOfLines
        Dim sProc As String
        
        Dim eProcKind As VBIDE.vbext_ProcKind
        
        sProc = cm.ProcOfLine(lLineLoop, eProcKind)
        
        If Not dicProcs.Exists(sProc) Then
            dicProcs.Add sProc, VBA.Switch(eProcKind = vbext_pk_Get, "get", eProcKind = vbext_pk_Let, "let", _
                                           eProcKind = vbext_pk_Set, "set", eProcKind = vbext_pk_Proc, "proc")
        End If
        
        If Not dotnetlistProcs.contains(sProc) Then
            dotnetlistProcs.Add sProc
        End If
        
    Next lLineLoop
    
    dotnetlistProcs.Sort
    
    Dim sJSON As String
    sJSON = VBA.Replace("{'compName':'%foo%','procs':[", "%foo%", vbc.Name)
    
    Dim l As Long
    For l = 0 To dotnetlistProcs.Count - 1
        sJSON = sJSON & "{'procName':" & "'" & VBA.IIf(Len(dotnetlistProcs.Item(l)) = 0, "(Declarations)", dotnetlistProcs.Item(l)) & "'," 
        sJSON = sJSON & "'procKind':" & "'" & dicProcs.Item(dotnetlistProcs.Item(l)) & "'}" & VBA.IIf(l <> dotnetlistProcs.Count - 1, ",", "")
        
    Next l
    
    sJSON = sJSON & "]}"
    
    ScanComponent = ParseAndStringify(sJSON)

End Function

Private Function ParseAndStringify(ByVal sJSON As String) As String
    '*
    '* this function callable by any code that concatenates JSON will parse and restringify (to reformat) if the module level debug
    '* flag is set to True
    '*
    If DebugJSON Then

        Dim oSC As ScriptControl
        Set oSC = SC
        
        Dim objParsed As Object
        Set objParsed = oSC.Run("JSON_parse", VBA.Replace(sJSON, "'", """")) '* JSON strictly has double quotes not single quotes
        
        Dim sReStringified As String
        ParseAndStringify = oSC.Run("JSON_stringify", objParsed)
        Debug.Print ParseAndStringify
    Else
        ParseAndStringify = VBA.Replace(sJSON, "'", """")
    End If

End Function

Private Function SC() As ScriptControl
    '*
    '* This ScriptControl hosts javascript fragments, some added here, some downloaded from web
    '*
    Static soSC As ScriptControl
    If soSC Is Nothing Then

        Set soSC = New ScriptControl
        soSC.Language = "JScript"

        soSC.AddCode GetJavaScriptLibrary("https://raw.githubusercontent.com/douglascrockford/JSON-js/master/json2.js")
        soSC.AddCode "function JSON_stringify(jsonObj) { return JSON.stringify(jsonObj); } "
        soSC.AddCode "function JSON_parse(sJson) { return JSON.parse(sJson); } "
        soSC.AddCode "function deleteValueByKey(obj,keyName) { delete obj[keyName]; } "
        soSC.AddCode "function setValueByKey(obj,keyName, newValue) { obj[keyName]=newValue; } "
        soSC.AddCode "function enumKeysToMsDict(jsonObj,msDict) { for (var i in jsonObj) { msDict.Add(i,0); }  } "
        

    End If
    Set SC = soSC
End Function

Private Function GetJavaScriptLibrary(ByVal sURL As String) As String
    '*
    '* This assists the ScriptControl to download javascript library
    '*
    Dim xHTTPRequest As MSXML2.XMLHTTP60
    Set xHTTPRequest = New MSXML2.XMLHTTP60
    xHTTPRequest.Open "GET", sURL, False
    xHTTPRequest.send
    GetJavaScriptLibrary = xHTTPRequest.responseText

End Function

So the output of the program is a JSON string, if I pretty print this with the help of https://jsonformatter.curiousconcept.com/ .

 
{  
   "projectName":"SVGHelper",
   "classes":[  
      {  
         "compName":"Point",
         "procs":[  
            {  
               "procName":"(Declarations)",
               "procKind":"proc"
            },
            {  
               "procName":"SetPoint",
               "procKind":"proc"
            },
            {  
               "procName":"x",
               "procKind":"get"
            },
            {  
               "procName":"y",
               "procKind":"get"
            }
         ]
      },
      {  
         "compName":"Points",
         "procs":[  
            {  
               "procName":"(Declarations)",
               "procKind":"proc"
            },
            {  
               "procName":"AddPoint",
               "procKind":"proc"
            },
            {  
               "procName":"Class_Initialize",
               "procKind":"proc"
            },
            {  
               "procName":"Count",
               "procKind":"proc"
            },
            {  
               "procName":"CreatePoint",
               "procKind":"proc"
            },
            {  
               "procName":"Item",
               "procKind":"proc"
            },
            {  
               "procName":"LastPoint",
               "procKind":"proc"
            }
         ]
      },
      {  
         "compName":"SVGPath",
         "procs":[  
            {  
               "procName":"(Declarations)",
               "procKind":"proc"
            },
            {  
               "procName":"AddPoint",
               "procKind":"proc"
            },
            {  
               "procName":"Class_Initialize",
               "procKind":"proc"
            },
            {  
               "procName":"ClosePath",
               "procKind":"proc"
            },
            {  
               "procName":"D_Attribute",
               "procKind":"proc"
            },
            {  
               "procName":"ReflectInBothXAndY",
               "procKind":"proc"
            },
            {  
               "procName":"ReflectInX",
               "procKind":"proc"
            },
            {  
               "procName":"ReflectInY",
               "procKind":"proc"
            },
            {  
               "procName":"SetMove",
               "procKind":"proc"
            }
         ]
      },
      {  
         "compName":"UnionJack",
         "procs":[  
            {  
               "procName":"(Declarations)",
               "procKind":"proc"
            },
            {  
               "procName":"BlueTriangle",
               "procKind":"proc"
            },
            {  
               "procName":"Class_Initialize",
               "procKind":"proc"
            },
            {  
               "procName":"EnglishCross",
               "procKind":"proc"
            },
            {  
               "procName":"MyLargerBlueTriangle",
               "procKind":"proc"
            },
            {  
               "procName":"MySmallerBlueTriangle",
               "procKind":"proc"
            },
            {  
               "procName":"StPatricksCrossBlade",
               "procKind":"proc"
            }
         ]
      },
      {  
         "compName":"SVGTextMessageLengthCalculator",
         "procs":[  
            {  
               "procName":"(Declarations)",
               "procKind":"proc"
            },
            {  
               "procName":"CalculateChunks",
               "procKind":"proc"
            },
            {  
               "procName":"Class_Initialize",
               "procKind":"proc"
            },
            {  
               "procName":"CycleThroughTextMessage",
               "procKind":"proc"
            },
            {  
               "procName":"Initialise",
               "procKind":"proc"
            },
            {  
               "procName":"NavigateToTextMessageAndMeasureWidth",
               "procKind":"proc"
            },
            {  
               "procName":"Terminate",
               "procKind":"proc"
            },
            {  
               "procName":"WriteSVGTextFile",
               "procKind":"proc"
            }
         ]
      },
      {  
         "compName":"SVGTextMessageHeightCalculator",
         "procs":[  
            {  
               "procName":"(Declarations)",
               "procKind":"proc"
            },
            {  
               "procName":"ComputeHeight",
               "procKind":"proc"
            }
         ]
      }
   ],
   "modules":[  
      {  
         "compName":"modUnionJack",
         "procs":[  
            {  
               "procName":"(Declarations)",
               "procKind":"proc"
            },
            {  
               "procName":"CreateFromScratch",
               "procKind":"proc"
            }
         ]
      },
      {  
         "compName":"tstSVGTextMessageCal",
         "procs":[  
            {  
               "procName":"(Declarations)",
               "procKind":"proc"
            },
            {  
               "procName":"TestHeight",
               "procKind":"proc"
            },
            {  
               "procName":"TestLengthCalculator",
               "procKind":"proc"
            }
         ]
      },
      {  
         "compName":"Module1",
         "procs":[  

         ]
      }
   ]
}

The previous iteration of code took each class method and converted into to a REST url but this means (i) tons more network calls; (ii) exposing URLs ; (iii) forcing into REST url paradigm. (Code not given).

http://localhost:1337/VBAModeller/SVGHelper/Classes/Point/
http://localhost:1337/VBAModeller/SVGHelper/Classes/Point/SetPoint
http://localhost:1337/VBAModeller/SVGHelper/Classes/Point/x_propget
http://localhost:1337/VBAModeller/SVGHelper/Classes/Point/y_propget
http://localhost:1337/VBAModeller/SVGHelper/Classes/Points/
http://localhost:1337/VBAModeller/SVGHelper/Classes/Points/AddPoint
http://localhost:1337/VBAModeller/SVGHelper/Classes/Points/Class_Initialize
http://localhost:1337/VBAModeller/SVGHelper/Classes/Points/Count
http://localhost:1337/VBAModeller/SVGHelper/Classes/Points/CreatePoint
http://localhost:1337/VBAModeller/SVGHelper/Classes/Points/Item
http://localhost:1337/VBAModeller/SVGHelper/Classes/Points/LastPoint
http://localhost:1337/VBAModeller/SVGHelper/Classes/SVGPath/
http://localhost:1337/VBAModeller/SVGHelper/Classes/SVGPath/AddPoint
http://localhost:1337/VBAModeller/SVGHelper/Classes/SVGPath/Class_Initialize
http://localhost:1337/VBAModeller/SVGHelper/Classes/SVGPath/ClosePath
http://localhost:1337/VBAModeller/SVGHelper/Classes/SVGPath/D_Attribute
http://localhost:1337/VBAModeller/SVGHelper/Classes/SVGPath/ReflectInBothXAndY
http://localhost:1337/VBAModeller/SVGHelper/Classes/SVGPath/ReflectInX
http://localhost:1337/VBAModeller/SVGHelper/Classes/SVGPath/ReflectInY
http://localhost:1337/VBAModeller/SVGHelper/Classes/SVGPath/SetMove
http://localhost:1337/VBAModeller/SVGHelper/Classes/UnionJack/
http://localhost:1337/VBAModeller/SVGHelper/Classes/UnionJack/BlueTriangle
http://localhost:1337/VBAModeller/SVGHelper/Classes/UnionJack/Class_Initialize
http://localhost:1337/VBAModeller/SVGHelper/Classes/UnionJack/EnglishCross
http://localhost:1337/VBAModeller/SVGHelper/Classes/UnionJack/MyLargerBlueTriangle
http://localhost:1337/VBAModeller/SVGHelper/Classes/UnionJack/MySmallerBlueTriangle
http://localhost:1337/VBAModeller/SVGHelper/Classes/UnionJack/StPatricksCrossBlade
http://localhost:1337/VBAModeller/SVGHelper/Classes/SVGTextMessageLengthCalculator/
http://localhost:1337/VBAModeller/SVGHelper/Classes/SVGTextMessageLengthCalculator/CalculateChunks
http://localhost:1337/VBAModeller/SVGHelper/Classes/SVGTextMessageLengthCalculator/Class_Initialize
http://localhost:1337/VBAModeller/SVGHelper/Classes/SVGTextMessageLengthCalculator/CycleThroughTextMessage
http://localhost:1337/VBAModeller/SVGHelper/Classes/SVGTextMessageLengthCalculator/Initialise
http://localhost:1337/VBAModeller/SVGHelper/Classes/SVGTextMessageLengthCalculator/NavigateToTextMessageAndMeasureWidth
http://localhost:1337/VBAModeller/SVGHelper/Classes/SVGTextMessageLengthCalculator/Terminate
http://localhost:1337/VBAModeller/SVGHelper/Classes/SVGTextMessageLengthCalculator/WriteSVGTextFile
http://localhost:1337/VBAModeller/SVGHelper/Classes/SVGTextMessageHeightCalculator/
http://localhost:1337/VBAModeller/SVGHelper/Classes/SVGTextMessageHeightCalculator/ComputeHeight
http://localhost:1337/VBAModeller/SVGHelper/Modules/modUnionJack/
http://localhost:1337/VBAModeller/SVGHelper/Modules/modUnionJack/CreateFromScratch
http://localhost:1337/VBAModeller/SVGHelper/Modules/tstSVGTextMessageCal/
http://localhost:1337/VBAModeller/SVGHelper/Modules/tstSVGTextMessageCal/TestHeight
http://localhost:1337/VBAModeller/SVGHelper/Modules/tstSVGTextMessageCal/TestLengthCalculator

No comments:

Post a Comment