├── .gitignore ├── images ├── logo.png └── wrm-schema.png ├── example ├── webxcel.xlsm ├── style-fixes.css ├── index.html └── app.jsx ├── constants.ps1 ├── log.ps1 ├── variables.ps1 ├── src ├── Modules │ ├── FastCGI.bas │ ├── Main.bas │ ├── StatusCode.bas │ ├── TestMarshal.bas │ ├── TestUtils.bas │ ├── PathExtensions.bas │ ├── TestStringExtensions.bas │ ├── Marshal.bas │ ├── HttpStatusCode.bas │ ├── wsock32.bas │ └── StringExtensions.bas └── Classes │ ├── IJson.cls │ ├── FastCGIParam.cls │ ├── WorksheetTableColumnHeader.cls │ ├── IFastCGIRecord.cls │ ├── IWebController.cls │ ├── JsonValue.cls │ ├── HttpHeader.cls │ ├── WebControllerCollection.cls │ ├── FastCGITypeInfo.cls │ ├── HttpHeaderCollection.cls │ ├── WorksheetTableCollection.cls │ ├── JsonArray.cls │ ├── FastCGIStream.cls │ ├── Assert.cls │ ├── FastCGIHeader.cls │ ├── JsonObject.cls │ ├── FastCGIBeginRequest.cls │ ├── FastCGIEndRequest.cls │ ├── FileInfo.cls │ ├── TcpClient.cls │ ├── HttpServer.cls │ ├── HttpResponse.cls │ ├── FastCGIParams.cls │ ├── FileSystemWebController.cls │ ├── TcpServer.cls │ ├── FastCGIClient.cls │ ├── FastCGIWebController.cls │ ├── HttpRequest.cls │ ├── WorksheetTable.cls │ ├── WorkbookWebController.cls │ ├── JsonParser.cls │ └── WorksheetRelationshipMapper.cls ├── export.ps1 ├── LICENSE ├── test.ps1 └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | build/ -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelneu/webxcel/HEAD/images/logo.png -------------------------------------------------------------------------------- /example/webxcel.xlsm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelneu/webxcel/HEAD/example/webxcel.xlsm -------------------------------------------------------------------------------- /images/wrm-schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelneu/webxcel/HEAD/images/wrm-schema.png -------------------------------------------------------------------------------- /constants.ps1: -------------------------------------------------------------------------------- 1 | $COMPONENT_TYPE_MODULE = 1 2 | $COMPONENT_TYPE_CLASS = 2 3 | $XL_FILE_FORMAT_MACRO_ENABLED = 52 4 | -------------------------------------------------------------------------------- /log.ps1: -------------------------------------------------------------------------------- 1 | Function LogInfo($message) 2 | { 3 | Write-Host $message -ForegroundColor Gray 4 | } 5 | 6 | Function LogEmptyLine() 7 | { 8 | echo "" 9 | } 10 | -------------------------------------------------------------------------------- /variables.ps1: -------------------------------------------------------------------------------- 1 | $CWD = (Resolve-Path .\).Path 2 | $BUILD_DIRECTORY = [IO.Path]::Combine($CWD, "build") 3 | $FILENAME = [IO.Path]::Combine($BUILD_DIRECTORY, "webxcel.xlsm") 4 | -------------------------------------------------------------------------------- /src/Modules/FastCGI.bas: -------------------------------------------------------------------------------- 1 | Attribute VB_Name = "FastCGI" 2 | Public Const FASTCGI_TYPE_BEGIN_REQUEST = 1 3 | Public Const FASTCGI_TYPE_END_REQUEST = 3 4 | Public Const FASTCGI_TYPE_PARAMS = 4 5 | Public Const FASTCGI_TYPE_STDIN = 5 6 | Public Const FASTCGI_TYPE_STDOUT = 6 7 | -------------------------------------------------------------------------------- /src/Classes/IJson.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "IJson" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Public Function ToJson() As String 11 | End Function 12 | -------------------------------------------------------------------------------- /src/Classes/FastCGIParam.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "FastCGIParam" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Public key As String 11 | Public value As String 12 | -------------------------------------------------------------------------------- /src/Classes/WorksheetTableColumnHeader.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "WorksheetTableColumnHeader" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Public index As Integer 11 | Public name As String 12 | -------------------------------------------------------------------------------- /src/Classes/IFastCGIRecord.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "IFastCGIRecord" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Public Sub WriteToTcpClient(client As TcpClient) 11 | End Sub 12 | 13 | Public Sub ReadFromTcpClient(client As TcpClient) 14 | End Sub 15 | -------------------------------------------------------------------------------- /src/Classes/IWebController.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "IWebController" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Public Function MatchesUrl(requestUrl As String) As Boolean 11 | End Function 12 | 13 | 14 | Public Function ProcessRequest(request As HttpRequest) As HttpResponse 15 | End Function 16 | -------------------------------------------------------------------------------- /src/Modules/Main.bas: -------------------------------------------------------------------------------- 1 | Attribute VB_Name = "Main" 2 | Public Sub Main() 3 | Dim server As HttpServer 4 | Set server = New HttpServer 5 | 6 | Dim php As FastCGIWebController 7 | Set php = New FastCGIWebController 8 | 9 | php.host = "localhost" 10 | php.port = 9000 11 | php.Extension = "*.php" 12 | 13 | server.Controllers.AddController php 14 | server.Controllers.AddController New WorkbookWebController 15 | server.Controllers.AddController New FileSystemWebController 16 | 17 | server.Serve 8080 18 | End Sub 19 | -------------------------------------------------------------------------------- /example/style-fixes.css: -------------------------------------------------------------------------------- 1 | .top-nav { 2 | height: 5em; 3 | font-size: 2.75em; 4 | margin-bottom: 1em; 5 | } 6 | 7 | .page-title { 8 | line-height: 7em; 9 | } 10 | 11 | .card .card-content input[type="checkbox"] + label { 12 | overflow-x: hidden; 13 | } 14 | 15 | .card .card-content input[type="checkbox"] + label .title { 16 | text-overflow: ellipsis; 17 | white-space: nowrap; 18 | overflow: hidden; 19 | width: 50vw; 20 | } 21 | 22 | .card .card-content input[type="checkbox"]:checked + label { 23 | text-decoration: line-through; 24 | color: #999 !important; 25 | } 26 | 27 | .l3 { 28 | text-align: right; 29 | } -------------------------------------------------------------------------------- /src/Modules/StatusCode.bas: -------------------------------------------------------------------------------- 1 | Attribute VB_Name = "StatusCode" 2 | Public Const ErrorSocketSetup = -1 3 | Public Const ErrorSocketCreation = -2 4 | Public Const ErrorSocketBind = -3 5 | Public Const ErrorSocketStartListening = -4 6 | Public Const ErrorSocketAcceptClient = -5 7 | 8 | Public Const ErrorHttpRequestInvalidFormat = -11 9 | Public Const ErrorHttpRequestUnknownRequestMethod = -12 10 | 11 | Public Const ErrorFileNotFound = -21 12 | 13 | Public Const ErrorSheetNotFound = -31 14 | Public Const ErrorNoPrimaryKeyDefined = -32 15 | Public Const ErrorInvalidForeignKeyUsed = -33 16 | Public Const ErrorDataDoesntContainPrimaryKey = -34 17 | 18 | Public Const ErrorMalformedJson = -41 19 | -------------------------------------------------------------------------------- /src/Classes/JsonValue.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "JsonValue" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Implements IJson 11 | 12 | Public value As Variant 13 | 14 | Public Property Get ContainsNull() As Boolean 15 | ContainsNull = IsNull(value) 16 | End Property 17 | 18 | Private Function IJson_ToJson() As String 19 | If TypeName(value) = "String" Then 20 | IJson_ToJson = """" & value & """" 21 | ElseIf ContainsNull Then 22 | IJson_ToJson = "null" 23 | Else 24 | IJson_ToJson = value 25 | End If 26 | End Function 27 | -------------------------------------------------------------------------------- /src/Classes/HttpHeader.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "HttpHeader" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Private m_name As String 11 | Private m_value As String 12 | 13 | 14 | Public Property Get name() As String 15 | name = m_name 16 | End Property 17 | 18 | 19 | Public Property Let name(ByVal value As String) 20 | m_name = value 21 | End Property 22 | 23 | 24 | Public Property Get value() As String 25 | value = m_value 26 | End Property 27 | 28 | 29 | Public Property Let value(ByVal val As String) 30 | m_value = val 31 | End Property 32 | -------------------------------------------------------------------------------- /src/Modules/TestMarshal.bas: -------------------------------------------------------------------------------- 1 | Attribute VB_Name = "TestMarshal" 2 | Public Function TestMarshalInt8() As Assert 3 | Dim value As Byte 4 | value = 10 5 | 6 | Set TestMarshalInt8 = Assert.AreEqual(value, Marshal.BytesToInt8(Marshal.Int8ToBytes(value)), "marshals int8") 7 | End Function 8 | 9 | Public Function TestMarshalInt16() As Assert 10 | Dim value As Long 11 | value = 10 12 | 13 | Set TestMarshalInt16 = Assert.AreEqual(value, Marshal.BytesToInt16(Marshal.Int16ToBytes(value)), "marshals int16") 14 | End Function 15 | 16 | Public Function TestMarshalInt32() As Assert 17 | Dim value As Long 18 | value = 10 19 | 20 | Set TestMarshalInt32 = Assert.AreEqual(value, Marshal.BytesToInt32(Marshal.Int32ToBytes(value)), "marshals int32") 21 | End Function 22 | -------------------------------------------------------------------------------- /src/Modules/TestUtils.bas: -------------------------------------------------------------------------------- 1 | Attribute VB_Name = "TestUtils" 2 | Public Type TestFileHandle 3 | TestFilename As String 4 | TestFilestream As Variant 5 | End Type 6 | 7 | ' Creates a temporary test file. 8 | Public Function CreateTestFile() As TestFileHandle 9 | Dim fso 10 | Set fso = CreateObject("Scripting.FileSystemObject") 11 | 12 | CreateTestFile.TestFilename = fso.BuildPath(ActiveDocument.Path, fso.GetTempName()) 13 | Set CreateTestFile.TestFilestream = fso.CreateTextFile(CreateTestFile.TestFilename, True) 14 | End Function 15 | 16 | 17 | ' Deletes the given test file. 18 | ' Arguments: 19 | ' - fileHandle The file handle to delete 20 | Public Sub DeleteTestFile(ByRef fileHandle As TestFileHandle) 21 | Dim fso 22 | Set fso = CreateObject("Scripting.FileSystemObject") 23 | 24 | fso.DeleteFile fileHandle.TestFilename 25 | End Sub 26 | -------------------------------------------------------------------------------- /src/Modules/PathExtensions.bas: -------------------------------------------------------------------------------- 1 | Attribute VB_Name = "PathExtensions" 2 | Public Function PathJoin(ParamArray pathParts() As Variant) As String 3 | If IsEmpty(pathParts) Then 4 | Exit Function 5 | End If 6 | 7 | Dim partIndex As Integer 8 | Dim pathPart As String 9 | Dim joinedPath As String 10 | joinedPath = "" 11 | 12 | For partIndex = LBound(pathParts) To UBound(pathParts) - 1 13 | pathPart = pathParts(partIndex) 14 | pathPart = TrimRight(pathPart, "/") 15 | pathPart = TrimRight(pathPart, "\") 16 | 17 | joinedPath = joinedPath & pathPart & "/" 18 | Next 19 | 20 | pathPart = pathParts(UBound(pathParts)) 21 | 22 | If UBound(pathParts) > 1 Then 23 | pathPart = TrimLeft(pathPart, "/") 24 | pathPart = TrimLeft(pathPart, "\") 25 | End If 26 | 27 | joinedPath = joinedPath & pathPart 28 | PathJoin = joinedPath 29 | End Function 30 | -------------------------------------------------------------------------------- /src/Classes/WebControllerCollection.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "WebControllerCollection" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Private m_controllers As Collection 11 | 12 | 13 | Private Sub Class_Initialize() 14 | Set m_controllers = New Collection 15 | End Sub 16 | 17 | 18 | Public Sub AddController(controller As IWebController) 19 | m_controllers.Add controller 20 | End Sub 21 | 22 | 23 | Public Function GetMatchingController(requestUrl As String) As IWebController 24 | For Each controller In m_controllers 25 | If controller.MatchesUrl(requestUrl) Then 26 | Set GetMatchingController = controller 27 | Exit Function 28 | End If 29 | Next controller 30 | 31 | Set GetMatchingController = Nothing 32 | End Function 33 | -------------------------------------------------------------------------------- /src/Classes/FastCGITypeInfo.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "FastCGITypeInfo" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Implements IFastCGIRecord 11 | 12 | Public ProtocolVersion As Byte 13 | Public MessageType As Byte 14 | 15 | 16 | Private Sub IFastCGIRecord_ReadFromTcpClient(client As TcpClient) 17 | Dim bytes As String 18 | bytes = client.ReceiveBytes(2) 19 | 20 | ProtocolVersion = Marshal.BytesToInt8(bytes) 21 | bytes = StringExtensions.Substring(bytes, 1) 22 | 23 | MessageType = Marshal.BytesToInt8(bytes) 24 | End Sub 25 | 26 | 27 | Private Sub IFastCGIRecord_WriteToTcpClient(client As TcpClient) 28 | Dim bytes As String 29 | bytes = "" 30 | 31 | bytes = bytes & Marshal.Int8ToBytes(ProtocolVersion) 32 | bytes = bytes & Marshal.Int8ToBytes(MessageType) 33 | 34 | client.SendString bytes 35 | End Sub 36 | -------------------------------------------------------------------------------- /export.ps1: -------------------------------------------------------------------------------- 1 | . .\variables.ps1 2 | . .\constants.ps1 3 | . .\log.ps1 4 | 5 | LogInfo "Collecting modules" 6 | 7 | $missing = [System.Reflection.Missing]::Value 8 | $excel = New-Object -ComObject Excel.Application 9 | $book = $excel.Workbooks.Open($FILENAME, $missing, $true) 10 | $modules = $book.VBProject.VBComponents; 11 | $exportedModules = 0 12 | 13 | For ($moduleIndex = 0; $moduleIndex -lt $modules.Count; $moduleIndex++) 14 | { 15 | $module = $modules.Item($moduleIndex + 1) 16 | $moduleFilename = switch ($module.Type) 17 | { 18 | $COMPONENT_TYPE_MODULE { "src\Modules\$($module.Name).bas" } 19 | $COMPONENT_TYPE_CLASS { "src\Classes\$($module.Name).cls" } 20 | default { "" } 21 | } 22 | 23 | if ($moduleFilename -eq "") 24 | { 25 | echo "skipping module '$($module.Name)'" 26 | continue 27 | } 28 | 29 | $moduleDestination = [IO.Path]::Combine($CWD, $moduleFilename) 30 | echo "exporting $moduleFilename" 31 | $module.Export($moduleDestination) 32 | $exportedModules += 1 33 | } 34 | 35 | $excel.Quit() 36 | LogInfo "Exported $exportedModules modules" 37 | LogEmptyLine 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright © 2017 michaelneu 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | webxcel-react-todo 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Classes/HttpHeaderCollection.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "HttpHeaderCollection" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Private m_headers 11 | 12 | 13 | Private Sub Class_Initialize() 14 | Set m_headers = CreateObject("Scripting.Dictionary") 15 | End Sub 16 | 17 | 18 | Public Sub AddHeader(ByVal name As String, ByVal value As String) 19 | If m_headers.Exists(name) Then 20 | m_headers.Remove name 21 | End If 22 | 23 | m_headers.Add name, value 24 | End Sub 25 | 26 | 27 | Public Function GetEnumerator() As Collection 28 | Dim headerCollection As Collection 29 | Set headerCollection = New Collection 30 | 31 | For Each key In m_headers.Keys 32 | Dim header As HttpHeader 33 | Set header = New HttpHeader 34 | 35 | header.name = key 36 | header.value = m_headers(key) 37 | 38 | headerCollection.Add header 39 | Next key 40 | 41 | Set GetEnumerator = headerCollection 42 | End Function 43 | 44 | -------------------------------------------------------------------------------- /src/Classes/WorksheetTableCollection.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "WorksheetTableCollection" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Private m_tables 11 | 12 | 13 | Private Sub Class_Initialize() 14 | Set m_tables = CreateObject("Scripting.Dictionary") 15 | 16 | Dim i As Integer 17 | For i = 1 To Worksheets.count 18 | Dim Sheet 19 | Set Sheet = Worksheets(i) 20 | 21 | Dim sheetName As String 22 | sheetName = LCase(Trim(Sheet.name)) 23 | 24 | Dim table As WorksheetTable 25 | Set table = New WorksheetTable 26 | table.Initialize Sheet 27 | 28 | m_tables.Add sheetName, table 29 | Next i 30 | End Sub 31 | 32 | 33 | Public Function FindTable(tableName As String) 34 | tableName = LCase(Trim(tableName)) 35 | 36 | If Not m_tables.Exists(tableName) Then 37 | Err.Raise StatusCode.ErrorSheetNotFound 38 | End If 39 | 40 | Set FindTable = m_tables(tableName) 41 | End Function 42 | -------------------------------------------------------------------------------- /src/Classes/JsonArray.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "JsonArray" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Implements IJson 11 | 12 | Private m_items As Collection 13 | 14 | 15 | Private Sub Class_Initialize() 16 | Set m_items = New Collection 17 | End Sub 18 | 19 | 20 | Public Property Get count() As Long 21 | count = m_items.count 22 | End Property 23 | 24 | 25 | Public Sub AddItem(item As IJson) 26 | m_items.Add item 27 | End Sub 28 | 29 | 30 | Public Function GetItem(index As Integer) As IJson 31 | Set GetItem = m_items(index + 1) 32 | End Function 33 | 34 | 35 | Private Function IJson_ToJson() As String 36 | Dim json As String 37 | Dim i As Integer 38 | 39 | For i = 1 To count 40 | Dim obj As IJson 41 | Set obj = m_items(i) 42 | 43 | json = json & obj.ToJson 44 | 45 | If i < count Then 46 | json = json & ", " 47 | End If 48 | Next i 49 | 50 | IJson_ToJson = "[" & json & "]" 51 | End Function 52 | -------------------------------------------------------------------------------- /src/Modules/TestStringExtensions.bas: -------------------------------------------------------------------------------- 1 | Attribute VB_Name = "TestStringExtensions" 2 | Public Function TestTrimLeft() As Assert 3 | Set TestTrimLeft = Assert.AreEqual("bar", StringExtensions.TrimLeft("oobar", "o"), "left-trims strings") 4 | End Function 5 | 6 | 7 | Public Function TestTrimRight() As Assert 8 | Set TestTrimRight = Assert.AreEqual("f", StringExtensions.TrimRight("foo", "o"), "right-trims strings") 9 | End Function 10 | 11 | 12 | Public Function TestStartsWith() As Assert 13 | Set TestStartsWith = Assert.IsTrue(StringExtensions.StartsWith("foobar", "foo"), "detects string starts") 14 | End Function 15 | 16 | 17 | Public Function TestEndsWith() As Assert 18 | Set TestEndsWith = Assert.IsTrue(StringExtensions.EndsWith("foobar", "bar"), "detects string ends") 19 | End Function 20 | 21 | 22 | Public Function TestCharAt() As Assert 23 | Set TestCharAt = Assert.AreEqual("a", StringExtensions.CharAt("foobar", 5), "gets chars from strings") 24 | End Function 25 | 26 | 27 | Public Function TestSubstring() As Assert 28 | Set TestSubstring = Assert.AreEqual("oo", StringExtensions.Substring("foobar", 1, 2), "gets parts from strings") 29 | End Function 30 | 31 | 32 | Public Function TestRepeat() As Assert 33 | Set TestRepeat = Assert.AreEqual("aaa", StringExtensions.Repeat("a", 3), "repeats strings") 34 | End Function 35 | 36 | -------------------------------------------------------------------------------- /src/Classes/FastCGIStream.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "FastCGIStream" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Implements IFastCGIRecord 11 | 12 | Private m_header As FastCGIHeader 13 | Public Content As String 14 | 15 | 16 | Private Sub Class_Initialize() 17 | Set m_header = New FastCGIHeader 18 | m_header.Info.ProtocolVersion = 1 19 | m_header.RequestId = 1 20 | m_header.PaddingLength = 0 21 | End Sub 22 | 23 | 24 | Public Property Get StreamType() As Integer 25 | StreamType = m_header.Info.MessageType 26 | End Property 27 | 28 | 29 | Public Property Let StreamType(ByVal value As Integer) 30 | If value = FastCGI.FASTCGI_TYPE_STDIN Or value <> FastCGI.FASTCGI_TYPE_STDOUT Then 31 | m_header.Info.MessageType = value 32 | End If 33 | End Property 34 | 35 | 36 | Private Sub IFastCGIRecord_ReadFromTcpClient(client As TcpClient) 37 | Dim record As IFastCGIRecord 38 | Set record = m_header 39 | record.ReadFromTcpClient client 40 | 41 | Content = client.ReceiveBytes(m_header.ContentLength) 42 | client.ReceiveBytes m_header.PaddingLength 43 | End Sub 44 | 45 | 46 | Private Sub IFastCGIRecord_WriteToTcpClient(client As TcpClient) 47 | m_header.ContentLength = Len(Content) 48 | 49 | Dim record As IFastCGIRecord 50 | Set record = m_header 51 | record.WriteToTcpClient client 52 | 53 | client.SendString Content 54 | End Sub 55 | -------------------------------------------------------------------------------- /src/Classes/Assert.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "Assert" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = True 9 | Attribute VB_Exposed = True 10 | ' Indicates whether the assertion was successful 11 | Public AssertSuccessful As Boolean 12 | ' The message of this assertion 13 | Public AssertMessage As String 14 | 15 | 16 | ' Asserts that both values are equal. If not, this will return a failed assertion. 17 | ' Arguments: 18 | ' - expected The value the caller expects to have 19 | ' - actual The actual value 20 | ' - message A message to show in case the assertion failed 21 | Public Function AreEqual(ByRef expected, ByRef actual, ByRef message As String) As Assert 22 | Set AreEqual = IsTrue(Not expected <> actual, message) 23 | 24 | If Not AreEqual.AssertSuccessful Then 25 | AreEqual.AssertMessage = AreEqual.AssertMessage & vbCrLf & " Expected: " & expected & vbCrLf & " Actual: " & actual 26 | End If 27 | End Function 28 | 29 | 30 | ' Asserts that the given value is true. 31 | ' Arguments: 32 | ' - truthy A value that can be seen as truthy. Very likely should be boolean, but maybe you like to live dangerously 33 | ' - message A message to show in case the assertion failed 34 | Public Function IsTrue(ByRef truthy, ByRef message As String) As Assert 35 | Set IsTrue = New Assert 36 | IsTrue.AssertSuccessful = True 37 | IsTrue.AssertMessage = message 38 | 39 | If Not truthy Then 40 | IsTrue.AssertSuccessful = False 41 | IsTrue.AssertMessage = " Assert failed: " & message 42 | End If 43 | End Function 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/Classes/FastCGIHeader.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "FastCGIHeader" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Implements IFastCGIRecord 11 | 12 | Private m_typeInfo As FastCGITypeInfo 13 | Public RequestId As Integer 14 | Public ContentLength As Long 15 | Public PaddingLength As Byte 16 | Public Reserved As String 17 | 18 | 19 | Private Sub Class_Initialize() 20 | Set m_typeInfo = New FastCGITypeInfo 21 | Reserved = StringExtensions.Repeat(Chr(0), 1) 22 | End Sub 23 | 24 | 25 | Public Property Get Info() As FastCGITypeInfo 26 | Set Info = m_typeInfo 27 | End Property 28 | 29 | 30 | Private Sub IFastCGIRecord_ReadFromTcpClient(client As TcpClient) 31 | Dim bytes As String 32 | bytes = client.ReceiveBytes(6) 33 | 34 | RequestId = Marshal.BytesToInt16(bytes) 35 | bytes = StringExtensions.Substring(bytes, 2) 36 | 37 | ContentLength = Marshal.BytesToInt16(bytes) 38 | bytes = StringExtensions.Substring(bytes, 2) 39 | 40 | PaddingLength = Marshal.BytesToInt8(bytes) 41 | bytes = StringExtensions.Substring(bytes, 1) 42 | 43 | Reserved = bytes 44 | End Sub 45 | 46 | 47 | Private Sub IFastCGIRecord_WriteToTcpClient(client As TcpClient) 48 | Dim bytes As String 49 | bytes = "" 50 | 51 | bytes = bytes & Marshal.Int8ToBytes(m_typeInfo.ProtocolVersion) 52 | bytes = bytes & Marshal.Int8ToBytes(m_typeInfo.MessageType) 53 | bytes = bytes & Marshal.Int16ToBytes(RequestId) 54 | bytes = bytes & Marshal.Int16ToBytes(ContentLength) 55 | bytes = bytes & Marshal.Int8ToBytes(PaddingLength) 56 | bytes = bytes & Reserved 57 | 58 | client.SendString bytes 59 | End Sub 60 | -------------------------------------------------------------------------------- /src/Classes/JsonObject.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "JsonObject" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Implements IJson 11 | 12 | Private m_properties 13 | 14 | 15 | Private Sub Class_Initialize() 16 | Set m_properties = CreateObject("Scripting.Dictionary") 17 | End Sub 18 | 19 | 20 | Public Property Get PropertyNames() As Collection 21 | Set PropertyNames = New Collection 22 | 23 | For Each property In m_properties 24 | PropertyNames.Add property 25 | Next property 26 | End Property 27 | 28 | 29 | Public Sub SetProperty(ByVal property As String, value As IJson) 30 | RemoveProperty property 31 | m_properties.Add property, value 32 | End Sub 33 | 34 | 35 | Public Sub RemoveProperty(ByVal property As String) 36 | If m_properties.Exists(property) Then 37 | m_properties.Remove property 38 | End If 39 | End Sub 40 | 41 | 42 | Public Function GetProperty(ByVal property As String) As IJson 43 | Set GetProperty = m_properties(property) 44 | End Function 45 | 46 | 47 | Private Function IJson_ToJson() As String 48 | Dim i As Long 49 | Dim propertyCount As Long 50 | propertyCount = m_properties.count 51 | 52 | Dim json As String 53 | 54 | For Each property In m_properties 55 | Dim obj As IJson 56 | Set obj = m_properties(property) 57 | 58 | Dim t 59 | t = TypeName(obj) 60 | 61 | json = json & """" & property & """: " & obj.ToJson 62 | 63 | If i < propertyCount - 1 Then 64 | json = json & ", " 65 | End If 66 | 67 | i = i + 1 68 | Next property 69 | 70 | IJson_ToJson = "{" & json & "}" 71 | End Function 72 | -------------------------------------------------------------------------------- /src/Classes/FastCGIBeginRequest.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "FastCGIBeginRequest" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Implements IFastCGIRecord 11 | 12 | Private m_header As FastCGIHeader 13 | Public Role As Integer 14 | Public flags As Byte 15 | Public Reserved As String 16 | 17 | 18 | Private Sub Class_Initialize() 19 | Set m_header = New FastCGIHeader 20 | m_header.Info.ProtocolVersion = 1 21 | m_header.Info.MessageType = FastCGI.FASTCGI_TYPE_BEGIN_REQUEST 22 | m_header.RequestId = 1 23 | m_header.ContentLength = 8 24 | m_header.PaddingLength = 0 25 | 26 | Role = 1 27 | Reserved = StringExtensions.Repeat(Chr(0), 5) 28 | End Sub 29 | 30 | 31 | Private Sub IFastCGIRecord_ReadFromTcpClient(client As TcpClient) 32 | Dim record As IFastCGIRecord 33 | Set record = m_header 34 | record.ReadFromTcpClient client 35 | 36 | Dim bytes As String 37 | bytes = client.ReceiveBytes(m_header.ContentLength) 38 | 39 | Role = Marshal.BytesToInt16(StringExtensions.Substring(bytes, 0, 2)) 40 | bytes = StringExtensions.Substring(bytes, 2) 41 | 42 | flags = Marshal.BytesToInt8(StringExtensions.Substring(bytes, 0, 1)) 43 | bytes = StringExtensions.Substring(bytes, 1) 44 | 45 | Reserved = bytes 46 | 47 | client.ReceiveBytes m_header.PaddingLength 48 | End Sub 49 | 50 | 51 | Private Sub IFastCGIRecord_WriteToTcpClient(client As TcpClient) 52 | Dim header As IFastCGIRecord 53 | Set header = m_header 54 | header.WriteToTcpClient client 55 | 56 | Dim bytes As String 57 | bytes = "" 58 | 59 | bytes = bytes & Marshal.Int16ToBytes(Role) 60 | bytes = bytes & Marshal.Int8ToBytes(flags) 61 | bytes = bytes & Reserved 62 | 63 | client.SendString bytes 64 | End Sub 65 | -------------------------------------------------------------------------------- /src/Classes/FastCGIEndRequest.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "FastCGIEndRequest" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Implements IFastCGIRecord 11 | 12 | Private m_header As FastCGIHeader 13 | Public AppStatus As Long 14 | Public ProtocolStatus As Byte 15 | Public Reserved As String 16 | 17 | 18 | Private Sub Class_Initialize() 19 | Set m_header = New FastCGIHeader 20 | m_header.Info.ProtocolVersion = 1 21 | m_header.Info.MessageType = FastCGI.FASTCGI_TYPE_END_REQUEST 22 | m_header.RequestId = 1 23 | m_header.ContentLength = 8 24 | m_header.PaddingLength = 0 25 | 26 | Reserved = StringExtensions.Repeat(Chr(0), 5) 27 | End Sub 28 | 29 | 30 | Private Sub IFastCGIRecord_ReadFromTcpClient(client As TcpClient) 31 | Dim record As IFastCGIRecord 32 | Set record = m_header 33 | record.ReadFromTcpClient client 34 | 35 | Dim bytes As String 36 | bytes = client.ReceiveBytes(m_header.ContentLength) 37 | 38 | AppStatus = Marshal.BytesToInt32(StringExtensions.Substring(bytes, 0, 4)) 39 | bytes = StringExtensions.Substring(bytes, 4) 40 | 41 | ProtocolStatus = Marshal.BytesToInt8(StringExtensions.Substring(bytes, 0, 1)) 42 | bytes = StringExtensions.Substring(bytes, 1) 43 | 44 | Reserved = bytes 45 | 46 | client.ReceiveBytes m_header.PaddingLength 47 | End Sub 48 | 49 | 50 | Private Sub IFastCGIRecord_WriteToTcpClient(client As TcpClient) 51 | Dim record As IFastCGIRecord 52 | Set record = m_header 53 | record.WriteToTcpClient client 54 | 55 | Dim bytes As String 56 | bytes = "" 57 | 58 | bytes = bytes & Marshal.Int32ToBytes(AppStatus) 59 | bytes = bytes & Marshal.Int8ToBytes(ProtocolStatus) 60 | bytes = bytes & Reserved 61 | 62 | client.SendString bytes 63 | End Sub 64 | -------------------------------------------------------------------------------- /src/Classes/FileInfo.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "FileInfo" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Private m_fso 11 | Private m_path As String 12 | 13 | 14 | Private Sub Class_Initialize() 15 | Set m_fso = CreateObject("Scripting.FileSystemObject") 16 | End Sub 17 | 18 | 19 | Public Sub Initialize(ByVal name As String) 20 | m_path = name 21 | End Sub 22 | 23 | 24 | Public Property Get Exists() As Boolean 25 | Exists = m_fso.FileExists(m_path) 26 | End Property 27 | 28 | 29 | Public Property Get Extension() As String 30 | Extension = m_fso.GetExtensionName(m_path) 31 | End Property 32 | 33 | 34 | Public Property Get MimeType() As String 35 | Dim ext As String 36 | ext = LCase(Extension) 37 | 38 | Select Case ext 39 | Case "json", "xml" 40 | MimeType = "application/" & ext 41 | 42 | Case "html", "htm", "txt" 43 | MimeType = "text/html" 44 | 45 | Case "png", "jpg", "jpeg", "gif" 46 | MimeType = "image/" & ext 47 | 48 | Case "css" 49 | MimeType = "text/css" 50 | 51 | Case Else 52 | MimeType = "application/octet-stream" 53 | End Select 54 | End Property 55 | 56 | 57 | Public Sub Create() 58 | Dim file 59 | Set file = m_fso.CreateTextFile(m_path) 60 | 61 | file.Close 62 | End Sub 63 | 64 | 65 | Public Sub WriteString(ByVal text As String) 66 | If Not Exists Then 67 | Create 68 | End If 69 | 70 | Dim file 71 | Set file = m_fso.OpenTextFile(m_path, 2) 72 | 73 | file.Write text 74 | file.Close 75 | End Sub 76 | 77 | 78 | Public Function ReadString() As String 79 | If Not Exists Then 80 | Err.Raise StatusCode.ErrorFileNotFound 81 | End If 82 | 83 | Dim file 84 | Set file = m_fso.OpenTextFile(m_path, 1) 85 | 86 | ReadString = file.ReadAll 87 | file.Close 88 | End Function 89 | -------------------------------------------------------------------------------- /src/Classes/TcpClient.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "TcpClient" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Option Explicit 11 | 12 | Private m_clientSocket As Long 13 | 14 | 15 | Public Sub ConnectTo(host As String, port As Long) 16 | If host = "localhost" Then 17 | host = "127.0.0.1" 18 | End If 19 | 20 | Dim address As wsock32.sockaddr_in 21 | address.sin_addr.s_addr = wsock32.inet_addr(host) 22 | address.sin_family = wsock32.AF_INET 23 | address.sin_port = wsock32.htons(port) 24 | 25 | m_clientSocket = wsock32.socket(wsock32.AF_INET, wsock32.SOCK_STREAM, 0) 26 | 27 | Dim connectResult As Long 28 | connectResult = wsock32.connect(m_clientSocket, address, 16) 29 | End Sub 30 | 31 | 32 | Public Sub Initialize(ByVal socket As Long) 33 | Dim timeout As Long 34 | timeout = 500 35 | 36 | Dim result As Long 37 | result = wsock32.setsockopt(socket, wsock32.SOL_SOCKET, wsock32.SO_RCVTIMEO, timeout, 4) 38 | 39 | m_clientSocket = socket 40 | End Sub 41 | 42 | 43 | Public Function SendString(ByVal message As String) As Long 44 | SendString = wsock32.send(m_clientSocket, ByVal message, Len(message), 0) 45 | End Function 46 | 47 | 48 | Public Function ReceiveBytes(ByVal bytes As Long) As String 49 | Dim buffer As String 50 | buffer = StringExtensions.Repeat(Chr(0), bytes) 51 | 52 | Dim readBytes As Long 53 | readBytes = wsock32.recv(m_clientSocket, buffer, bytes, 0) 54 | 55 | If readBytes <> -1 Then 56 | ReceiveBytes = StringExtensions.Substring(buffer, 0, readBytes) 57 | End If 58 | End Function 59 | 60 | 61 | Public Function ReceiveString() As String 62 | Dim buffer As String 63 | Dim message As String 64 | message = "" 65 | 66 | Do 67 | buffer = Trim(ReceiveBytes(1024)) 68 | 69 | If Len(buffer) > 0 Then 70 | message = message & buffer 71 | End If 72 | Loop While Len(buffer) > 0 73 | 74 | ReceiveString = Trim(message) 75 | End Function 76 | 77 | 78 | Public Sub Dispose() 79 | wsock32.closesocket m_clientSocket 80 | End Sub 81 | -------------------------------------------------------------------------------- /src/Modules/Marshal.bas: -------------------------------------------------------------------------------- 1 | Attribute VB_Name = "Marshal" 2 | Private Const integer0xFF As Integer = 256 3 | Private Const long0xFF As Long = 256 4 | 5 | 6 | ' Converts the given int8 to a big endian byte string. 7 | ' Arguments: 8 | ' - value The byte to convert 9 | Public Function Int8ToBytes(ByVal value As Byte) As String 10 | Int8ToBytes = Chr(value) 11 | End Function 12 | 13 | 14 | ' Converts the given big endian byte string to an int8 15 | ' Arguments: 16 | ' - bytes The bytes to convert 17 | Public Function BytesToInt8(ByVal bytes As String) As Byte 18 | BytesToInt8 = Asc(CharAt(bytes, 1)) 19 | End Function 20 | 21 | 22 | ' Converts the given int16 to a big endian byte string. 23 | ' Arguments: 24 | ' - value The byte to convert 25 | Public Function Int16ToBytes(ByVal value As Integer) As String 26 | Dim bytes As String * 2 27 | Dim rest As Integer 28 | 29 | rest = value Mod integer0xFF 30 | Mid(bytes, 2) = Chr(rest) 31 | 32 | value = (value - rest) / integer0xFF 33 | rest = value Mod integer0xFF 34 | Mid(bytes, 1) = Chr(rest) 35 | 36 | Int16ToBytes = bytes 37 | End Function 38 | 39 | 40 | ' Converts the given big endian byte string to an int16 41 | ' Arguments: 42 | ' - bytes The bytes to convert 43 | Public Function BytesToInt16(ByVal bytes As String) As Long 44 | BytesToInt16 = Asc(CharAt(bytes, 1)) * long0xFF + Asc(CharAt(bytes, 2)) 45 | End Function 46 | 47 | 48 | ' Converts the given int32 to a big endian byte string. 49 | ' Arguments: 50 | ' - value The byte to convert 51 | Public Function Int32ToBytes(ByVal value As Long) As String 52 | Dim bytes As String * 4 53 | Dim rest As Long 54 | 55 | rest = value Mod long0xFF 56 | Mid(bytes, 4) = Chr(rest) 57 | 58 | value = (value - rest) / long0xFF 59 | rest = value Mod long0xFF 60 | Mid(bytes, 3) = Chr(rest) 61 | 62 | value = (value - rest) / long0xFF 63 | rest = value Mod long0xFF 64 | Mid(bytes, 2) = Chr(rest) 65 | 66 | value = (value - rest) / long0xFF 67 | rest = value Mod long0xFF 68 | Mid(bytes, 1) = Chr(rest) 69 | 70 | Int32ToBytes = bytes 71 | End Function 72 | 73 | 74 | ' Converts the given big endian byte string to an int32 75 | ' Arguments: 76 | ' - bytes The bytes to convert 77 | Public Function BytesToInt32(ByVal bytes As String) As Long 78 | BytesToInt32 = Asc(CharAt(bytes, 1)) * long0xFF * long0xFF * long0xFF + Asc(CharAt(bytes, 2)) * long0xFF * long0xFF + Asc(CharAt(bytes, 3)) * long0xFF + Asc(CharAt(bytes, 4)) 79 | End Function 80 | -------------------------------------------------------------------------------- /src/Classes/HttpServer.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "HttpServer" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Private m_tcpServer As TcpServer 11 | Private m_controllers As WebControllerCollection 12 | 13 | 14 | Private Sub Class_Initialize() 15 | InitializeHttpStatusDictionary 16 | 17 | Set m_tcpServer = New TcpServer 18 | Set m_controllers = New WebControllerCollection 19 | End Sub 20 | 21 | 22 | Public Property Get Controllers() As WebControllerCollection 23 | Set Controllers = m_controllers 24 | End Property 25 | 26 | 27 | Public Sub Serve(ByVal port As Long) 28 | Dim lockfile As FileInfo 29 | Set lockfile = New FileInfo 30 | 31 | lockfile.Initialize ActiveWorkbook.FullName & ".lock" 32 | lockfile.Create 33 | 34 | m_tcpServer.BindTo port, 100 35 | 36 | Do While True 37 | Dim client As TcpClient 38 | Set client = m_tcpServer.AcceptTcpClient(1000) 39 | 40 | If Not client Is Nothing Then 41 | Dim requestText As String 42 | requestText = client.ReceiveString() 43 | 44 | If requestText <> "" Then 45 | Dim request As HttpRequest 46 | Set request = New HttpRequest 47 | request.Parse requestText 48 | 49 | Dim response As HttpResponse 50 | Set response = ProcessRequest(request) 51 | 52 | Dim responseText As String 53 | responseText = response.ToString() 54 | 55 | client.SendString responseText 56 | End If 57 | 58 | client.Dispose 59 | End If 60 | 61 | If Not lockfile.Exists Then 62 | Exit Do 63 | End If 64 | 65 | DoEvents 66 | Loop 67 | 68 | m_tcpServer.Dispose 69 | End Sub 70 | 71 | 72 | Private Function ProcessRequest(request As HttpRequest) As HttpResponse 73 | Dim controller As IWebController 74 | Set controller = Controllers.GetMatchingController(request.Url) 75 | 76 | If Not controller Is Nothing Then 77 | Set ProcessRequest = controller.ProcessRequest(request) 78 | Else 79 | Dim response As HttpResponse 80 | Set response = New HttpResponse 81 | 82 | response.StatusCode = 500 83 | 84 | Set ProcessRequest = response 85 | End If 86 | End Function 87 | -------------------------------------------------------------------------------- /src/Classes/HttpResponse.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "HttpResponse" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Private m_statusCode As Integer 11 | Private m_body As String 12 | Private m_headers As HttpHeaderCollection 13 | 14 | 15 | Private Sub Class_Initialize() 16 | Set m_headers = New HttpHeaderCollection 17 | End Sub 18 | 19 | 20 | Public Property Get StatusCode() As Integer 21 | StatusCode = m_statusCode 22 | End Property 23 | 24 | 25 | Public Property Let StatusCode(ByVal value As Integer) 26 | m_statusCode = value 27 | End Property 28 | 29 | 30 | Public Property Get body() As String 31 | body = m_body 32 | End Property 33 | 34 | 35 | Public Property Let body(ByVal value As String) 36 | m_body = value 37 | End Property 38 | 39 | 40 | Public Property Get Headers() As HttpHeaderCollection 41 | Set Headers = m_headers 42 | End Property 43 | 44 | 45 | Public Function ToString() As String 46 | Headers.AddHeader "Server", "Microsoft Excel/" & Application.Version 47 | Headers.AddHeader "Content-Length", Len(body) 48 | Headers.AddHeader "Connection", "close" 49 | 50 | Dim text As String 51 | text = "HTTP/1.1 " & StatusCode & " " & HttpStatusMessages(StatusCode) & vbCrLf 52 | 53 | For Each header In Headers.GetEnumerator 54 | text = text & header.name & ": " & header.value & vbCrLf 55 | Next header 56 | 57 | text = text & vbCrLf 58 | text = text & body 59 | 60 | ToString = text 61 | End Function 62 | 63 | 64 | Public Sub Parse(ByVal response As String) 65 | Dim parts 66 | parts = Split(response, vbCrLf & vbCrLf, 2) 67 | 68 | If UBound(parts) <> 1 Then 69 | Err.Raise ErrorHttpRequestInvalidFormat 70 | Exit Sub 71 | End If 72 | 73 | ParseHeaders parts(0) 74 | m_body = Trim(parts(1)) 75 | End Sub 76 | 77 | 78 | Private Sub ParseHeaders(ByVal Headers As String) 79 | Set m_headers = New HttpHeaderCollection 80 | 81 | Dim lines 82 | lines = Split(Headers, vbCrLf) 83 | 84 | For Each line In lines 85 | ParseHeader line 86 | Next 87 | End Sub 88 | 89 | 90 | Private Sub ParseHeader(ByVal line As String) 91 | line = Trim(line) 92 | 93 | Dim parts 94 | parts = Split(line, ":", 2) 95 | 96 | If parts(0) = "Status" Then 97 | StatusCode = Split(parts(0), " ", 1) 98 | Else 99 | m_headers.AddHeader Trim(parts(0)), Trim(parts(1)) 100 | End If 101 | End Sub 102 | -------------------------------------------------------------------------------- /src/Classes/FastCGIParams.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "FastCGIParams" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Implements IFastCGIRecord 11 | 12 | Private m_header As FastCGIHeader 13 | Private m_params As Collection 14 | 15 | 16 | Private Sub Class_Initialize() 17 | Set m_header = New FastCGIHeader 18 | m_header.Info.ProtocolVersion = 1 19 | m_header.Info.MessageType = FastCGI.FASTCGI_TYPE_PARAMS 20 | m_header.RequestId = 1 21 | m_header.PaddingLength = 0 22 | 23 | Set m_params = New Collection 24 | End Sub 25 | 26 | 27 | Public Sub Add(key As String, value As String) 28 | Dim param As FastCGIParam 29 | Set param = New FastCGIParam 30 | 31 | param.key = key 32 | param.value = value 33 | 34 | m_params.Add param 35 | End Sub 36 | 37 | 38 | Private Sub IFastCGIRecord_ReadFromTcpClient(client As TcpClient) 39 | Dim record As IFastCGIRecord 40 | Set record = m_header 41 | record.ReadFromTcpClient client 42 | 43 | Set m_params = New Collection 44 | 45 | Dim bytes As String 46 | bytes = client.ReceiveBytes(m_header.ContentLength) 47 | 48 | Dim keyLength As Integer 49 | Dim valueLength As Integer 50 | Dim param As FastCGIParam 51 | 52 | Do While Len(bytes) > 0 53 | keyLength = Marshal.BytesToInt8(StringExtensions.Substring(bytes, 0, 1)) 54 | bytes = StringExtensions.Substring(bytes, 1) 55 | valueLength = Marshal.BytesToInt8(StringExtensions.Substring(bytes, 0, 1)) 56 | bytes = StringExtensions.Substring(bytes, 1) 57 | 58 | Set param = New FastCGIParam 59 | 60 | param.key = StringExtensions.Substring(bytes, 0, keyLength) 61 | bytes = StringExtensions.Substring(bytes, keyLength) 62 | 63 | param.value = StringExtensions.Substring(bytes, 0, valueLength) 64 | bytes = StringExtensions.Substring(bytes, valueLength) 65 | 66 | m_params.Add param 67 | Loop 68 | 69 | client.ReceiveBytes m_header.PaddingLength 70 | End Sub 71 | 72 | 73 | Private Sub IFastCGIRecord_WriteToTcpClient(client As TcpClient) 74 | Dim record As IFastCGIRecord 75 | Dim bytes As String 76 | bytes = "" 77 | 78 | For Each param In m_params 79 | bytes = bytes & Marshal.Int8ToBytes(Len(param.key)) 80 | bytes = bytes & Marshal.Int8ToBytes(Len(param.value)) 81 | bytes = bytes & param.key 82 | bytes = bytes & param.value 83 | Next 84 | 85 | m_header.ContentLength = Len(bytes) 86 | Set record = m_header 87 | record.WriteToTcpClient client 88 | client.SendString bytes 89 | End Sub 90 | -------------------------------------------------------------------------------- /src/Classes/FileSystemWebController.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "FileSystemWebController" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Implements IWebController 11 | 12 | Private m_directory As String 13 | Private m_indexFiles As Collection 14 | 15 | 16 | Private Sub Class_Initialize() 17 | m_directory = ActiveWorkbook.Path 18 | 19 | Set m_indexFiles = New Collection 20 | m_indexFiles.Add "index.html" 21 | m_indexFiles.Add "index.htm" 22 | End Sub 23 | 24 | 25 | Public Property Get Directory() As String 26 | Directory = m_directory 27 | End Property 28 | 29 | 30 | Public Property Let Directory(val As String) 31 | val = StringExtensions.TrimRight(val, "/") 32 | val = StringExtensions.TrimRight(val, "\") 33 | 34 | m_directory = val 35 | End Property 36 | 37 | 38 | Public Property Get IndexFiles() As Collection 39 | Set IndexFiles = m_indexFiles 40 | End Property 41 | 42 | 43 | Private Function IWebController_MatchesUrl(requestUrl As String) As Boolean 44 | IWebController_MatchesUrl = True 45 | End Function 46 | 47 | 48 | Private Function IWebController_ProcessRequest(request As HttpRequest) As HttpResponse 49 | Dim response As HttpResponse 50 | Set response = New HttpResponse 51 | 52 | Dim filename As String 53 | filename = ResolveFile(request.Url) 54 | 55 | Dim file As FileInfo 56 | Set file = New FileInfo 57 | 58 | file.Initialize filename 59 | 60 | If file.Exists Then 61 | response.StatusCode = 200 62 | response.Headers.AddHeader "Content-Type", file.MimeType 63 | response.body = file.ReadString 64 | ElseIf m_indexFiles.count > 0 Then 65 | Dim foundIndexFile As Boolean 66 | foundIndexFile = False 67 | 68 | For Each indexFile In m_indexFiles 69 | Set file = New FileInfo 70 | file.Initialize PathJoin(filename, indexFile) 71 | 72 | If file.Exists Then 73 | response.StatusCode = 200 74 | response.Headers.AddHeader "Content-Type", file.MimeType 75 | response.body = file.ReadString 76 | 77 | foundIndexFile = True 78 | Exit For 79 | End If 80 | Next 81 | 82 | If foundIndexFile = False Then 83 | response.StatusCode = 404 84 | End If 85 | Else 86 | response.StatusCode = 404 87 | End If 88 | 89 | Set IWebController_ProcessRequest = response 90 | End Function 91 | 92 | 93 | Private Function ResolveFile(file As String) As String 94 | file = StringExtensions.TrimLeft(file, "/") 95 | file = StringExtensions.TrimLeft(file, "\") 96 | 97 | ResolveFile = m_directory & "\" & file 98 | End Function 99 | -------------------------------------------------------------------------------- /src/Classes/TcpServer.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "TcpServer" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Option Explicit 11 | 12 | Private m_wsa As wsock32.WSADATA 13 | Private m_serverSocket As Long 14 | Private m_fdSet As wsock32.fd_set 15 | 16 | 17 | Private Sub Class_Initialize() 18 | Dim result As Long 19 | result = wsock32.WSAStartup(257, m_wsa) 20 | 21 | If result <> 0 Then 22 | Err.Raise StatusCode.ErrorSocketSetup 23 | Exit Sub 24 | End If 25 | End Sub 26 | 27 | 28 | 29 | Public Sub BindTo(ByVal port As Long, Optional ByVal backlog As Integer = 10) 30 | m_serverSocket = wsock32.socket(AF_INET, SOCK_STREAM, 0) 31 | 32 | If m_serverSocket = wsock32.INVALID_SOCKET Then 33 | Err.Raise StatusCode.ErrorSocketCreation 34 | Exit Sub 35 | End If 36 | 37 | Dim endpoint As wsock32.sockaddr_in 38 | endpoint.sin_family = wsock32.AF_INET 39 | endpoint.sin_addr.s_addr = wsock32.INADDR_ANY 40 | endpoint.sin_port = wsock32.htons(port) 41 | 42 | Dim bindResult As Long 43 | bindResult = wsock32.bind(m_serverSocket, endpoint, 16) 44 | 45 | If bindResult <> 0 Then 46 | Dispose 47 | Err.Raise StatusCode.ErrorSocketBind 48 | Exit Sub 49 | End If 50 | 51 | Dim listenResult As Long 52 | listenResult = wsock32.listen(m_serverSocket, backlog) 53 | 54 | If listenResult <> 0 Then 55 | Dispose 56 | Err.Raise StatusCode.ErrorSocketStartListening 57 | Exit Sub 58 | End If 59 | End Sub 60 | 61 | 62 | Public Function AcceptTcpClient(Optional ByVal timeoutMs As Long = 500) As TcpClient 63 | wsock32.FD_ZERO_MACRO m_fdSet 64 | wsock32.FD_SET_MACRO m_serverSocket, m_fdSet 65 | 66 | Dim time As wsock32.timeval 67 | time.tv_sec = timeoutMs / 1000 68 | time.tv_usec = timeoutMs Mod 1000 69 | 70 | Dim emptyFdSet As fd_set 71 | 72 | Dim selectResult As Integer 73 | selectResult = wsock32.select_(m_serverSocket, m_fdSet, emptyFdSet, emptyFdSet, time) 74 | 75 | If selectResult = 0 Then 76 | Set AcceptTcpClient = Nothing 77 | Exit Function 78 | End If 79 | 80 | Dim socket 81 | Dim socketAddress As wsock32.sockaddr 82 | 83 | socket = wsock32.accept(m_serverSocket, socketAddress, 16) 84 | 85 | If socket = -1 Then 86 | Dispose 87 | Err.Raise StatusCode.ErrorSocketAcceptClient 88 | Exit Function 89 | End If 90 | 91 | Dim client As TcpClient 92 | Set client = New TcpClient 93 | 94 | client.Initialize socket 95 | 96 | Set AcceptTcpClient = client 97 | End Function 98 | 99 | 100 | Public Sub Dispose() 101 | wsock32.closesocket (m_serverSocket) 102 | wsock32.WSACleanup 103 | End Sub 104 | -------------------------------------------------------------------------------- /src/Classes/FastCGIClient.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "FastCGIClient" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Private m_clientSocket As TcpClient 11 | 12 | 13 | Public Sub Initialize(clientSocket As TcpClient) 14 | Set m_clientSocket = clientSocket 15 | End Sub 16 | 17 | 18 | Public Function ReadTypeInfo() As FastCGITypeInfo 19 | Dim record As IFastCGIRecord 20 | Set record = New FastCGITypeInfo 21 | record.ReadFromTcpClient m_clientSocket 22 | Set ReadTypeInfo = record 23 | End Function 24 | 25 | 26 | Public Sub WriteBegin() 27 | Dim record As IFastCGIRecord 28 | Set record = New FastCGIBeginRequest 29 | record.WriteToTcpClient m_clientSocket 30 | End Sub 31 | 32 | 33 | Public Function ReadBegin() As FastCGIBeginRequest 34 | Dim record As IFastCGIRecord 35 | Set record = New FastCGIBeginRequest 36 | record.ReadFromTcpClient m_clientSocket 37 | Set ReadBegin = record 38 | End Function 39 | 40 | 41 | Public Sub WriteParams(ByRef params As FastCGIParams) 42 | Dim record As IFastCGIRecord 43 | Set record = params 44 | record.WriteToTcpClient m_clientSocket 45 | 46 | Set record = New FastCGIParams 47 | record.WriteToTcpClient m_clientSocket 48 | End Sub 49 | 50 | 51 | Public Function ReadParams() As FastCGIParams 52 | Dim record As IFastCGIRecord 53 | Set record = New FastCGIParams 54 | record.ReadFromTcpClient m_clientSocket 55 | Set ReadParams = record 56 | End Function 57 | 58 | 59 | Public Sub WriteInput(text As String) 60 | Dim stdin As FastCGIStream 61 | Set stdin = New FastCGIStream 62 | stdin.StreamType = FastCGI.FASTCGI_TYPE_STDIN 63 | stdin.Content = text 64 | 65 | Dim bytes As String 66 | Dim record As IFastCGIRecord 67 | Set record = stdin 68 | record.WriteToTcpClient m_clientSocket 69 | 70 | If Len(text) > 0 Then 71 | stdin.Content = "" 72 | Set record = stdin 73 | record.WriteToTcpClient m_clientSocket 74 | End If 75 | End Sub 76 | 77 | 78 | Public Sub WriteOutput(text As String) 79 | Dim stdin As FastCGIStream 80 | Set stdin = New FastCGIStream 81 | stdin.StreamType = FastCGI.FASTCGI_TYPE_STDOUT 82 | stdin.Content = text 83 | 84 | Dim bytes As String 85 | Dim record As IFastCGIRecord 86 | Set record = stdin 87 | record.WriteToTcpClient m_clientSocket 88 | End Sub 89 | 90 | 91 | Public Function ReadStream() As FastCGIStream 92 | Dim record As IFastCGIRecord 93 | Set record = New FastCGIStream 94 | record.ReadFromTcpClient m_clientSocket 95 | Set ReadStream = record 96 | End Function 97 | 98 | 99 | Public Sub WriteEnd() 100 | Dim record As IFastCGIRecord 101 | Set record = New FastCGIEndRequest 102 | record.WriteToTcpClient m_clientSocket 103 | End Sub 104 | 105 | 106 | Public Function ReadEnd() As FastCGIEndRequest 107 | Dim record As IFastCGIRecord 108 | Set record = New FastCGIEndRequest 109 | record.ReadFromTcpClient m_clientSocket 110 | Set ReadEnd = record 111 | End Function 112 | -------------------------------------------------------------------------------- /src/Classes/FastCGIWebController.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "FastCGIWebController" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Implements IWebController 11 | 12 | 13 | Public host As String 14 | Public port As Long 15 | Public Extension As String 16 | 17 | 18 | Private Function IWebController_MatchesUrl(requestUrl As String) As Boolean 19 | If requestUrl Like Extension Then 20 | IWebController_MatchesUrl = True 21 | Else 22 | IWebController_MatchesUrl = False 23 | End If 24 | End Function 25 | 26 | 27 | Private Function IWebController_ProcessRequest(request As HttpRequest) As HttpResponse 28 | Dim clientSocket As TcpClient 29 | Set clientSocket = New TcpClient 30 | clientSocket.ConnectTo host, port 31 | 32 | Dim fcgiClient As FastCGIClient 33 | Set fcgiClient = New FastCGIClient 34 | fcgiClient.Initialize clientSocket 35 | 36 | fcgiClient.WriteBegin 37 | 38 | Dim params As FastCGIParams 39 | Set params = New FastCGIParams 40 | 41 | params.Add "REQUEST_METHOD", request.RequestMethod 42 | params.Add "SERVER_PROTOCOL", "http" 43 | params.Add "SERVER_NAME", "webxcel.local" 44 | params.Add "SERVER_SOFTWARE", "Microsoft Excel/" & Application.Version 45 | params.Add "GATEWAY_INTERFACE", "CGI/1.1" 46 | params.Add "SCRIPT_NAME", request.Url 47 | params.Add "SCRIPT_FILENAME", request.Url 48 | params.Add "DOCUMENT_URI", request.Url 49 | 50 | For Each header In request.Headers.GetEnumerator 51 | params.Add ConvertHeaderNameToParamName(header.name), header.value 52 | Next 53 | 54 | fcgiClient.WriteParams params 55 | Set params = New FastCGIParams 56 | fcgiClient.WriteParams params 57 | 58 | If Len(request.body) > 0 Then 59 | Dim stdin As String 60 | stdin = request.body 61 | 62 | Do While Len(stdin) > 65535 63 | Dim chunk As String 64 | chunk = StringExtensions.Substring(stdin, 0, 65535) 65 | fcgiClient.WriteInput chunk 66 | stdin = StringExtensions.Substring(stdin, 65535, 0) 67 | Loop 68 | End If 69 | 70 | fcgiClient.WriteInput "" 71 | 72 | Dim typeInfo As FastCGITypeInfo 73 | Dim body As String 74 | body = "" 75 | 76 | Do While True 77 | Set typeInfo = fcgiClient.ReadTypeInfo() 78 | 79 | If typeInfo.MessageType = FastCGI.FASTCGI_TYPE_STDOUT Then 80 | Dim stdout As FastCGIStream 81 | Set stdout = fcgiClient.ReadStream() 82 | body = body & stdout.Content 83 | ElseIf typeInfo.MessageType = FastCGI.FASTCGI_TYPE_END_REQUEST Then 84 | Dim endRequest As FastCGIEndRequest 85 | Set endRequest = fcgiClient.ReadEnd() 86 | 87 | Exit Do 88 | End If 89 | Loop 90 | 91 | clientSocket.Dispose 92 | 93 | Dim response As HttpResponse 94 | Set response = New HttpResponse 95 | response.Parse body 96 | 97 | If response.StatusCode = 0 Then 98 | response.StatusCode = 200 99 | End If 100 | 101 | Set IWebController_ProcessRequest = response 102 | End Function 103 | 104 | 105 | Private Function ConvertHeaderNameToParamName(ByVal headerName As String) As String 106 | ConvertHeaderNameToParamName = UCase(Replace(headerName, "-", "_")) 107 | End Function 108 | -------------------------------------------------------------------------------- /src/Classes/HttpRequest.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "HttpRequest" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Public Enum HttpRequestMethod 11 | MethodGet 12 | MethodPost 13 | MethodPut 14 | MethodDelete 15 | End Enum 16 | 17 | 18 | Private m_requestMethod As HttpRequestMethod 19 | Private m_url As String 20 | Private m_headers As HttpHeaderCollection 21 | Private m_body As String 22 | 23 | 24 | Private Sub Class_Initialize() 25 | Set m_headers = New HttpHeaderCollection 26 | End Sub 27 | 28 | 29 | Public Property Get RequestMethod() As HttpRequestMethod 30 | RequestMethod = m_requestMethod 31 | End Property 32 | 33 | 34 | Public Property Get Url() As String 35 | Url = m_url 36 | End Property 37 | 38 | 39 | Public Property Get body() As String 40 | body = m_body 41 | End Property 42 | 43 | 44 | Public Property Get Headers() As HttpHeaderCollection 45 | Set Headers = m_headers 46 | End Property 47 | 48 | 49 | Public Sub Parse(ByVal request As String) 50 | request = Trim(request) 51 | 52 | Dim parts 53 | parts = Split(request, vbCrLf & vbCrLf, 2) 54 | 55 | If UBound(parts) <> 1 Then 56 | Err.Raise StatusCode.ErrorHttpRequestInvalidFormat 57 | Exit Sub 58 | End If 59 | 60 | ParseHeaders parts(0) 61 | m_body = Trim(parts(1)) 62 | End Sub 63 | 64 | 65 | Private Sub ParseHeaders(text) 66 | text = Trim(text) 67 | 68 | Dim lines 69 | lines = Split(text, vbCrLf) 70 | 71 | Dim lineCount As Integer 72 | lineCount = UBound(lines) 73 | 74 | If lineCount = 0 Then 75 | Err.Raise StatusCode.ErrorHttpRequestInvalidFormat 76 | End If 77 | 78 | ParseProtocolLine lines(0) 79 | 80 | Dim i As Integer 81 | For i = 1 To lineCount 82 | ParseHeader lines(i) 83 | Next i 84 | End Sub 85 | 86 | 87 | Private Sub ParseProtocolLine(line) 88 | line = Trim(line) 89 | 90 | Dim upperLine As String 91 | upperLine = UCase(line) 92 | 93 | If Not upperLine Like "* HTTP/1.1" Then 94 | Err.Raise StatusCode.ErrorHttpRequestInvalidFormat 95 | End If 96 | 97 | Dim parts 98 | parts = Split(line, " ", 2) 99 | 100 | ParseRequestMethod parts(0) 101 | 102 | Dim urlPart As String 103 | urlPart = parts(1) 104 | 105 | Dim urlPartLength As Integer 106 | urlPartLength = Len(urlPart) 107 | 108 | Dim protocolSuffixLength As Integer 109 | protocolSuffixLength = Len(" HTTP/1.1") 110 | 111 | m_url = Left(urlPart, urlPartLength - protocolSuffixLength) 112 | End Sub 113 | 114 | 115 | Private Sub ParseHeader(line) 116 | line = Trim(line) 117 | 118 | Dim parts 119 | parts = Split(line, ":", 2) 120 | 121 | m_headers.AddHeader Trim(parts(0)), Trim(parts(1)) 122 | End Sub 123 | 124 | 125 | Private Sub ParseRequestMethod(method) 126 | method = LCase(method) 127 | 128 | If method = "get" Then 129 | m_requestMethod = MethodGet 130 | ElseIf method = "post" Then 131 | m_requestMethod = MethodPost 132 | ElseIf method = "put" Then 133 | m_requestMethod = MethodPut 134 | ElseIf method = "delete" Then 135 | m_requestMethod = MethodDelete 136 | Else 137 | Err.Raise StatusCode.ErrorHttpRequestUnknownRequestMethod 138 | End If 139 | End Sub 140 | -------------------------------------------------------------------------------- /src/Modules/HttpStatusCode.bas: -------------------------------------------------------------------------------- 1 | Attribute VB_Name = "HttpStatusCode" 2 | Option Private Module ' To hide the init sub in the macro list 3 | Public HttpStatusMessages As Object 4 | 5 | Sub InitializeHttpStatusDictionary() 6 | Set HttpStatusMessages = CreateObject("Scripting.Dictionary") 7 | HttpStatusMessages.Add 100, "Continue" 8 | HttpStatusMessages.Add 101, "Switching Protocols" 9 | HttpStatusMessages.Add 102, "Processing" 10 | HttpStatusMessages.Add 200, "OK" 11 | HttpStatusMessages.Add 201, "Created" 12 | HttpStatusMessages.Add 202, "Accepted" 13 | HttpStatusMessages.Add 203, "Non-authoritative Information" 14 | HttpStatusMessages.Add 204, "No Content" 15 | HttpStatusMessages.Add 205, "Reset Content" 16 | HttpStatusMessages.Add 206, "Partial Content" 17 | HttpStatusMessages.Add 207, "Multi-Status" 18 | HttpStatusMessages.Add 208, "Already Reported" 19 | HttpStatusMessages.Add 226, "IM Used" 20 | HttpStatusMessages.Add 300, "Multiple Choices" 21 | HttpStatusMessages.Add 301, "Moved Permanently" 22 | HttpStatusMessages.Add 302, "Found" 23 | HttpStatusMessages.Add 303, "See Other" 24 | HttpStatusMessages.Add 304, "Not Modified" 25 | HttpStatusMessages.Add 305, "Use Proxy" 26 | HttpStatusMessages.Add 307, "Temporary Redirect" 27 | HttpStatusMessages.Add 308, "Permanent Redirect" 28 | HttpStatusMessages.Add 400, "Bad Request" 29 | HttpStatusMessages.Add 401, "Unauthorized" 30 | HttpStatusMessages.Add 402, "Payment Required" 31 | HttpStatusMessages.Add 403, "Forbidden" 32 | HttpStatusMessages.Add 404, "Not Found" 33 | HttpStatusMessages.Add 405, "Method Not Allowed" 34 | HttpStatusMessages.Add 406, "Not Acceptable" 35 | HttpStatusMessages.Add 407, "Proxy Authentication Required" 36 | HttpStatusMessages.Add 408, "Request Timeout" 37 | HttpStatusMessages.Add 409, "Conflict" 38 | HttpStatusMessages.Add 410, "Gone" 39 | HttpStatusMessages.Add 411, "Length Required" 40 | HttpStatusMessages.Add 412, "Precondition Failed" 41 | HttpStatusMessages.Add 413, "Payload Too Large" 42 | HttpStatusMessages.Add 414, "Request-URI Too Long" 43 | HttpStatusMessages.Add 415, "Unsupported Media Type" 44 | HttpStatusMessages.Add 416, "Requested Range Not Satisfiable" 45 | HttpStatusMessages.Add 417, "Expectation Failed" 46 | HttpStatusMessages.Add 418, "I'm a teapot" 47 | HttpStatusMessages.Add 421, "Misdirected Request" 48 | HttpStatusMessages.Add 422, "Unprocessable Entity" 49 | HttpStatusMessages.Add 423, "Locked" 50 | HttpStatusMessages.Add 424, "Failed Dependency" 51 | HttpStatusMessages.Add 426, "Upgrade Required" 52 | HttpStatusMessages.Add 428, "Precondition Required" 53 | HttpStatusMessages.Add 429, "Too Many Requests" 54 | HttpStatusMessages.Add 431, "Request Header Fields Too Large" 55 | HttpStatusMessages.Add 444, "Connection Closed Without Response" 56 | HttpStatusMessages.Add 451, "Unavailable For Legal Reasons" 57 | HttpStatusMessages.Add 499, "Client Closed Request" 58 | HttpStatusMessages.Add 500, "Internal Server Error" 59 | HttpStatusMessages.Add 501, "Not Implemented" 60 | HttpStatusMessages.Add 502, "Bad Gateway" 61 | HttpStatusMessages.Add 503, "Service Unavailable" 62 | HttpStatusMessages.Add 504, "Gateway Timeout" 63 | HttpStatusMessages.Add 505, "HTTP Version Not Supported" 64 | HttpStatusMessages.Add 506, "Variant Also Negotiates" 65 | HttpStatusMessages.Add 507, "Insufficient Storage" 66 | HttpStatusMessages.Add 508, "Loop Detected" 67 | HttpStatusMessages.Add 510, "Not Extended" 68 | HttpStatusMessages.Add 511, "Network Authentication Required" 69 | HttpStatusMessages.Add 599, "Network Connect Timeout Error" 70 | End Sub 71 | -------------------------------------------------------------------------------- /src/Modules/wsock32.bas: -------------------------------------------------------------------------------- 1 | Attribute VB_Name = "wsock32" 2 | Public Const WSADESCRIPTION_LEN = 256 3 | Public Const WSASYS_STATUS_LEN = 128 4 | 5 | Public Const WSADESCRIPTION_LEN_ARRAY = WSADESCRIPTION_LEN + 1 6 | Public Const WSASYS_STATUS_LEN_ARRAY = WSASYS_STATUS_LEN + 1 7 | 8 | Public Type WSADATA 9 | wVersion As Integer 10 | wHighVersion As Integer 11 | szDescription As String * WSADESCRIPTION_LEN_ARRAY 12 | szSystemStatus As String * WSASYS_STATUS_LEN_ARRAY 13 | iMaxSockets As Integer 14 | iMaxUdpDg As Integer 15 | lpVendorInfo As String 16 | End Type 17 | 18 | Public Const AF_INET = 2 19 | Public Const SOCK_STREAM = 1 20 | Public Const INADDR_ANY = 0 21 | 22 | Public Type IN_ADDR 23 | s_addr As Long 24 | End Type 25 | 26 | Public Type sockaddr_in 27 | sin_family As Integer 28 | sin_port As Integer 29 | sin_addr As IN_ADDR 30 | sin_zero As String * 8 31 | End Type 32 | 33 | Public Const FD_SETSIZE = 64 34 | 35 | Public Type fd_set 36 | fd_count As Integer 37 | fd_array(FD_SETSIZE) As Long 38 | End Type 39 | 40 | Public Type timeval 41 | tv_sec As Long 42 | tv_usec As Long 43 | End Type 44 | 45 | Public Type sockaddr 46 | sa_family As Integer 47 | sa_data As String * 14 48 | End Type 49 | 50 | Public Const INVALID_SOCKET = -1 51 | 52 | Public Const SOL_SOCKET = 65535 53 | Public Const SO_RCVTIMEO = &H1006 54 | 55 | Public Declare PtrSafe Function WSAStartup Lib "wsock32.dll" (ByVal versionRequired As Long, wsa As WSADATA) As Long 56 | Public Declare PtrSafe Function WSAGetLastError Lib "wsock32.dll" () As Long 57 | Public Declare PtrSafe Function WSACleanup Lib "wsock32.dll" () As Long 58 | Public Declare PtrSafe Function socket Lib "wsock32.dll" (ByVal addressFamily As Long, ByVal socketType As Long, ByVal protocol As Long) As Long 59 | Public Declare PtrSafe Function connect Lib "wsock32.dll" (ByVal s As Long, ByRef address As sockaddr_in, ByVal namelen As Long) As Long 60 | Public Declare PtrSafe Function htons Lib "wsock32.dll" (ByVal hostshort As Long) As Integer 61 | Public Declare PtrSafe Function bind Lib "wsock32.dll" (ByVal socket As Long, name As sockaddr_in, ByVal nameLength As Integer) As Long 62 | Public Declare PtrSafe Function listen Lib "wsock32.dll" (ByVal socket As Long, ByVal backlog As Integer) As Long 63 | Public Declare PtrSafe Function select_ Lib "wsock32.dll" Alias "select" (ByVal nfds As Integer, readfds As fd_set, writefds As fd_set, exceptfds As fd_set, timeout As timeval) As Integer 64 | Public Declare PtrSafe Function accept Lib "wsock32.dll" (ByVal socket As Long, clientAddress As sockaddr, clientAddressLength As Integer) As Long 65 | Public Declare PtrSafe Function setsockopt Lib "wsock32.dll" (ByVal socket As Long, ByVal level As Long, ByVal optname As Long, ByRef optval As Long, ByVal optlen As Integer) As Long 66 | Public Declare PtrSafe Function send Lib "wsock32.dll" (ByVal socket As Long, buffer As String, ByVal bufferLength As Long, ByVal flags As Long) As Long 67 | Public Declare PtrSafe Function recv Lib "wsock32.dll" (ByVal socket As Long, ByVal buffer As String, ByVal bufferLength As Long, ByVal flags As Long) As Long 68 | Public Declare PtrSafe Function inet_addr Lib "wsock32.dll" (ByVal hostname As String) As Long 69 | Public Declare PtrSafe Function closesocket Lib "wsock32.dll" (ByVal s As Long) As Long 70 | 71 | 72 | Public Sub FD_ZERO_MACRO(ByRef s As fd_set) 73 | s.fd_count = 0 74 | End Sub 75 | 76 | 77 | Public Sub FD_SET_MACRO(ByVal fd As Long, ByRef s As fd_set) 78 | Dim i As Integer 79 | i = 0 80 | 81 | Do While i < s.fd_count 82 | If s.fd_array(i) = fd Then 83 | Exit Do 84 | End If 85 | 86 | i = i + 1 87 | Loop 88 | 89 | If i = s.fd_count Then 90 | If s.fd_count < FD_SETSIZE Then 91 | s.fd_array(i) = fd 92 | s.fd_count = s.fd_count + 1 93 | End If 94 | End If 95 | End Sub 96 | -------------------------------------------------------------------------------- /src/Modules/StringExtensions.bas: -------------------------------------------------------------------------------- 1 | Attribute VB_Name = "StringExtensions" 2 | ' Trims the given character from the given text starting from the left. 3 | ' Arguments: 4 | ' - text The text to trim 5 | ' - c The character to trim 6 | Public Function TrimLeft(ByVal text As String, c As String) As String 7 | Dim textLength As Long 8 | textLength = Len(text) 9 | 10 | Dim firstCharacter As String 11 | 12 | Do While textLength > 0 13 | firstCharacter = Left(text, 1) 14 | 15 | If firstCharacter <> c Then 16 | Exit Do 17 | End If 18 | 19 | text = Right(text, textLength - 1) 20 | textLength = Len(text) 21 | Loop 22 | 23 | TrimLeft = text 24 | End Function 25 | 26 | 27 | ' Trims the given character from the given text starting from the right. 28 | ' Arguments: 29 | ' - text The text to trim 30 | ' - c The character to trim 31 | Public Function TrimRight(ByVal text As String, c As String) As String 32 | Dim textLength As Long 33 | textLength = Len(text) 34 | 35 | Dim lastCharacter As String 36 | 37 | Do While textLength > 0 38 | lastCharacter = Right(text, 1) 39 | 40 | If lastCharacter <> c Then 41 | Exit Do 42 | End If 43 | 44 | text = Left(text, textLength - 1) 45 | textLength = Len(text) 46 | Loop 47 | 48 | TrimRight = text 49 | End Function 50 | 51 | 52 | ' Gets the substring from the given text. 53 | ' Arguments: 54 | ' - text The text to get the substring from 55 | ' - startIndex The index of the first character of the substring 56 | ' - [length] The amount of characters to take from the original string 57 | Public Function Substring(ByVal text As String, ByVal startIndex As Integer, Optional ByVal length As Variant) As String 58 | If startIndex > Len(text) Then 59 | startIndex = Len(text) 60 | End If 61 | 62 | If IsMissing(length) Then 63 | length = Len(text) - startIndex 64 | End If 65 | 66 | If length > Len(text) Then 67 | length = Len(text) - startIndex 68 | End If 69 | 70 | Substring = Left(Right(text, Len(text) - startIndex), length) 71 | End Function 72 | 73 | 74 | ' Checks whether the given text starts with the given sequence. 75 | ' Arguments: 76 | ' - text The text to check for the sequence 77 | ' - startText The text to be located at the start 78 | Public Function StartsWith(ByVal text As String, ByVal startText As String) As Boolean 79 | StartsWith = InStr(text, startText) = 1 80 | End Function 81 | 82 | 83 | ' Checks whether the given text ends with the given sequence. 84 | ' Arguments: 85 | ' - text The text to check for the sequence 86 | ' - endText The text to be located at the end 87 | Public Function EndsWith(ByVal text As String, ByVal endText As String) As Boolean 88 | EndsWith = Right(text, Len(endText)) = endText 89 | End Function 90 | 91 | 92 | ' Gets the character at the given index from the given string. 93 | ' Arguments: 94 | ' - text The text to get the character from 95 | ' - index The index of the character to get 96 | Public Function CharAt(ByVal text As String, ByVal index As Integer) As String 97 | CharAt = Mid(text, index, 1) 98 | End Function 99 | 100 | 101 | ' Repeats the given string the given amount of times. 102 | ' Arguments: 103 | ' - text The text to repeat 104 | ' - count The amount of times to repeat the given string 105 | Public Function Repeat(ByVal text As String, ByVal count As Long) As String 106 | Repeat = "" 107 | 108 | Dim i As Long 109 | For i = 1 To count 110 | Repeat = Repeat & text 111 | Next 112 | End Function 113 | 114 | 115 | ' Converts a regular string "foo" to a L"foo" string. 116 | ' Arguments: 117 | ' - text The string to convert 118 | Public Function StringToWideString(ByVal text As String) As String 119 | StringToWideString = StrConv(text, vbUnicode) 120 | End Function 121 | -------------------------------------------------------------------------------- /example/app.jsx: -------------------------------------------------------------------------------- 1 | class App extends React.Component { 2 | render() { 3 | return ( 4 |
5 | 12 | 13 |
14 | 15 |
16 |
17 | ) 18 | } 19 | }; 20 | 21 | 22 | class ItemAddForm extends React.Component { 23 | constructor(props) { 24 | super(props); 25 | 26 | this.state = { 27 | value: "" 28 | }; 29 | 30 | this.onAddButtonClicked = this.onAddButtonClicked.bind(this); 31 | this.handleInput = this.handleInput.bind(this); 32 | } 33 | 34 | onAddButtonClicked() { 35 | const value = this.state.value.trim(); 36 | 37 | if (value.length > 0) { 38 | this.props.onItemAdded(value); 39 | this.setState({ value: "" }); 40 | } 41 | } 42 | 43 | handleInput(event) { 44 | this.setState({ value: event.target.value }); 45 | } 46 | 47 | render() { 48 | return ( 49 |
50 |
51 | 52 |
53 | 54 |
55 |
56 | 57 | add 58 | 59 |
60 |
61 | ); 62 | } 63 | } 64 | 65 | 66 | class TodoList extends React.Component { 67 | constructor(props) { 68 | super(props); 69 | 70 | this.state = { 71 | items: [] 72 | }; 73 | 74 | this.addItem = this.addItem.bind(this); 75 | this.itemDeleted = this.itemDeleted.bind(this); 76 | } 77 | 78 | componentDidMount() { 79 | axios.get("/workbook/todo") 80 | .then(response => this.setState({ 81 | items: response.data.map(item => { 82 | item.checked = item.checked == "TRUE"; 83 | 84 | return item; 85 | }) 86 | })); 87 | } 88 | 89 | addItem(title) { 90 | const id = uuid(), 91 | item = { 92 | id, 93 | title, 94 | checked: false 95 | }; 96 | 97 | axios.post("/workbook/todo", item); 98 | 99 | this.state.items.push(item); 100 | this.setState(this.state); 101 | } 102 | 103 | itemCheckedChanged(id, checked) { 104 | axios.put(`/workbook/todo/${id}`, { 105 | id, 106 | checked 107 | }); 108 | } 109 | 110 | itemDeleted(id) { 111 | const items = this.state.items.filter(item => item.id != id); 112 | 113 | this.setState({ items }); 114 | axios.delete(`/workbook/todo/${id}`); 115 | } 116 | 117 | render() { 118 | const items = this.state.items.map(item => ( 119 | 120 | )); 121 | 122 | return ( 123 |
124 | 125 | 126 |
127 | 128 |
129 |
130 | {items} 131 |
132 |
133 |
134 | ); 135 | } 136 | } 137 | 138 | 139 | class TodoItem extends React.Component { 140 | constructor(props) { 141 | super(props); 142 | 143 | this.state = { 144 | checked: props.checked 145 | }; 146 | 147 | this.onChange = this.onChange.bind(this); 148 | this.onDeleted = this.onDeleted.bind(this); 149 | } 150 | 151 | onChange() { 152 | const id = this.props.id, 153 | checked = !this.state.checked; 154 | 155 | this.setState({ checked }); 156 | this.props.onCheckedChanged(id, checked); 157 | } 158 | 159 | onDeleted() { 160 | const id = this.props.id; 161 | 162 | this.props.onDeleted(id); 163 | } 164 | 165 | render() { 166 | return ( 167 |
168 |
169 |
170 | 171 | 174 |
175 |
176 | delete 177 |
178 |
179 |
180 | ); 181 | } 182 | } 183 | 184 | 185 | ReactDOM.render( 186 | , 187 | document.getElementById("app") 188 | ); -------------------------------------------------------------------------------- /test.ps1: -------------------------------------------------------------------------------- 1 | . .\variables.ps1 2 | . .\constants.ps1 3 | . .\log.ps1 4 | 5 | LogInfo "Collecting tests" 6 | 7 | $missing = [System.Reflection.Missing]::Value 8 | $excel = New-Object -ComObject Excel.Application 9 | $book = $excel.Workbooks.Open($FILENAME, $missing, $true) 10 | $modules = $book.VBProject.VBComponents; 11 | $suites = @{} 12 | $suiteFlags = @{} 13 | 14 | $SUITE_FLAG_BEFORE_ALL = 1 15 | $SUITE_FLAG_AFTER_ALL = 2 16 | $SUITE_FLAG_BEFORE_EACH = 4 17 | $SUITE_FLAG_AFTER_EACH = 8 18 | 19 | $testCount = 0 20 | 21 | For ($moduleIndex = 0; $moduleIndex -lt $modules.Count; $moduleIndex++) 22 | { 23 | # vba interop seems to be written in VB6 => indices start at 1 24 | $module = $modules.Item($moduleIndex + 1) 25 | 26 | If (!$module.Name.StartsWith("Test")) 27 | { 28 | Continue 29 | } 30 | 31 | $code = $module.CodeModule.Lines(1, $module.CodeModule.CountOfLines) 32 | $lines = $code.Split("`r`n") 33 | 34 | $suiteFlags[$module.Name] = 0 35 | 36 | ForEach ($line in $lines) 37 | { 38 | If ($line.StartsWith("Public Function Test")) 39 | { 40 | If (!$suites.ContainsKey($module.Name)) 41 | { 42 | $suites[$module.Name] = [System.Collections.ArrayList]@() 43 | } 44 | 45 | $testName = $line.Split(" ")[2].Trim("()") 46 | $_ = $suites[$module.Name].Add($testName) 47 | $testCount += 1 48 | } 49 | 50 | If ($line.StartsWith("Public Sub BeforeAll()")) 51 | { 52 | $suiteFlags[$module.Name] = $suiteFlags[$module.Name] -bor $SUITE_FLAG_BEFORE_ALL 53 | } 54 | 55 | If ($line.StartsWith("Public Sub AfterAll()")) 56 | { 57 | $suiteFlags[$module.Name] = $suiteFlags[$module.Name] -bor $SUITE_FLAG_AFTER_ALL 58 | } 59 | 60 | If ($line.StartsWith("Public Sub BeforeEach()")) 61 | { 62 | $suiteFlags[$module.Name] = $suiteFlags[$module.Name] -bor $SUITE_FLAG_BEFORE_EACH 63 | } 64 | 65 | If ($line.StartsWith("Public Sub AfterEach()")) 66 | { 67 | $suiteFlags[$module.Name] = $suiteFlags[$module.Name] -bor $SUITE_FLAG_AFTER_EACH 68 | } 69 | } 70 | } 71 | 72 | LogInfo "Found $($suites.Count) suites with $testCount tests" 73 | LogEmptyLine 74 | 75 | $passedSuites = 0 76 | $passedTests = 0 77 | 78 | Function HasSuiteFlag($flags, $flag) 79 | { 80 | Return ($flags -band $flag) -eq $flag 81 | } 82 | 83 | ForEach ($suite in $suites.Keys) 84 | { 85 | $successful = $true 86 | $tests = $suites[$suite] 87 | $flags = $suiteFlags[$suite] 88 | 89 | $title = $suite + " (" + $tests.Count + " tests)" 90 | echo $title 91 | 92 | $hasBeforeAll = HasSuiteFlag $flags $SUITE_FLAG_BEFORE_ALL 93 | $hasAfterAll = HasSuiteFlag $flags $SUITE_FLAG_AFTER_ALL 94 | $hasBeforeEach = HasSuiteFlag $flags $SUITE_FLAG_BEFORE_EACH 95 | $hasAfterEach = HasSuiteFlag $flags $SUITE_FLAG_AFTER_EACH 96 | 97 | If ($hasBeforeAll) 98 | { 99 | $excel.Run($suite + "." + "BeforeAll") 100 | } 101 | 102 | ForEach ($test in $tests) 103 | { 104 | If ($hasBeforeEach) 105 | { 106 | $excel.Run($suite + "." + "BeforeEach") 107 | } 108 | 109 | $result = $excel.Run($suite + "." + $test) 110 | 111 | Write-Host " " -NoNewline 112 | 113 | if ($result.AssertSuccessful) 114 | { 115 | $passedTests += 1 116 | Write-Host " PASS " -BackgroundColor Green -ForegroundColor White -NoNewline 117 | } 118 | else 119 | { 120 | $successful = $false 121 | Write-Host " FAIL " -BackgroundColor Red -ForegroundColor White -NoNewline 122 | } 123 | 124 | Write-Host " $test" -NoNewline 125 | 126 | if ($result.AssertSuccessful) 127 | { 128 | Write-Host ": $($result.AssertMessage)" -ForegroundColor Gray 129 | } 130 | else 131 | { 132 | LogEmptyLine 133 | LogEmptyLine 134 | Write-Host $result.AssertMessage -ForegroundColor Red 135 | LogEmptyLine 136 | } 137 | 138 | If ($hasAfterEach) 139 | { 140 | $excel.Run($suite + "." + "AfterEach") 141 | } 142 | } 143 | 144 | If ($hasAfterAll) 145 | { 146 | $excel.Run($suite + "." + "AfterAll") 147 | } 148 | 149 | if ($successful) 150 | { 151 | $passedSuites += 1 152 | } 153 | 154 | echo "" 155 | } 156 | 157 | $excel.Quit() 158 | 159 | Function LogSummary($title, $passed, $failed) 160 | { 161 | Write-Host $title -NoNewline 162 | Write-Host "$passed passed" -ForegroundColor Green -NoNewline 163 | 164 | if ($failed -gt 0) 165 | { 166 | Write-Host ", " -NoNewline 167 | Write-Host "${failed} failed" -ForegroundColor Red -NoNewline 168 | } 169 | 170 | $total = $passed + $failed 171 | Write-Host ", $total total" 172 | } 173 | 174 | LogSummary "Test suites: " $passedSuites ($suites.Count - $passedSuites) 175 | LogSummary "Tests: " $passedTests ($testCount - $passedTests) 176 | LogInfo "Ran all test suites." 177 | LogEmptyLine 178 | -------------------------------------------------------------------------------- /src/Classes/WorksheetTable.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "WorksheetTable" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Private Type ColumnHeader 11 | name As String 12 | index As Integer 13 | End Type 14 | 15 | 16 | Private m_sheet 17 | Private m_columns As Collection 18 | Private m_primaryKey As Integer 19 | 20 | 21 | Public Property Get PrimaryKey() As String 22 | For Each column In m_columns 23 | If column.index = m_primaryKey Then 24 | PrimaryKey = column.name 25 | Exit Property 26 | End If 27 | Next column 28 | End Property 29 | 30 | 31 | Public Property Get Columns() As Collection 32 | Set Columns = New Collection 33 | 34 | For Each column In m_columns 35 | Columns.Add column.name 36 | Next column 37 | End Property 38 | 39 | 40 | Public Property Get Entries() As Collection 41 | Set Entries = New Collection 42 | 43 | Dim i As Long 44 | For i = 2 To m_sheet.Rows.count 45 | Dim row 46 | Set row = m_sheet.Rows(i) 47 | 48 | Dim pkText 49 | pkText = Trim(row.Cells(m_primaryKey)) 50 | 51 | If pkText = "" Then 52 | Exit For 53 | End If 54 | 55 | Dim entry 56 | Set entry = CreateObject("Scripting.Dictionary") 57 | 58 | For Each column In m_columns 59 | Dim val 60 | val = Trim(row.Cells(column.index).text) 61 | 62 | If val = "" Then 63 | entry(column.name) = Null 64 | Else 65 | entry(column.name) = val 66 | End If 67 | Next column 68 | 69 | Entries.Add entry 70 | Next i 71 | End Property 72 | 73 | 74 | Public Sub Insert(value) 75 | Dim insertionRow As Long 76 | 77 | insertionRow = m_sheet.Cells(1, m_primaryKey).End(xlDown).row + 1 78 | 79 | If insertionRow = 1048577 Then 80 | For insertionRow = 2 To 1048576 81 | Dim text 82 | text = m_sheet.Cells(insertionRow, m_primaryKey).text 83 | If Trim(text) = "" Then 84 | Exit For 85 | End If 86 | Next insertionRow 87 | End If 88 | 89 | SetRowValues insertionRow, value 90 | End Sub 91 | 92 | 93 | Public Function Delete(key As String) 94 | Set Delete = Nothing 95 | 96 | Dim i As Long 97 | For i = 2 To m_sheet.Rows.count 98 | Dim row 99 | Set row = m_sheet.Rows(i) 100 | 101 | Dim pkText 102 | pkText = Trim(row.Cells(m_primaryKey)) 103 | 104 | If pkText = key Then 105 | Dim entry 106 | Set entry = CreateObject("Scripting.Dictionary") 107 | 108 | For Each column In m_columns 109 | Dim val 110 | val = Trim(row.Cells(column.index).text) 111 | 112 | If val = "" Then 113 | entry(column.name) = Null 114 | Else 115 | entry(column.name) = val 116 | End If 117 | Next column 118 | 119 | row.Delete 120 | Set Delete = entry 121 | 122 | Exit Function 123 | End If 124 | Next i 125 | End Function 126 | 127 | 128 | Public Function Update(key As String, values) As Boolean 129 | Update = False 130 | 131 | Dim updateRow As Long 132 | For updateRow = 2 To m_sheet.Rows.count 133 | Dim row 134 | Set row = m_sheet.Rows(updateRow) 135 | 136 | Dim pkText 137 | pkText = Trim(row.Cells(m_primaryKey)) 138 | 139 | If pkText = key Then 140 | SetRowValues updateRow, values 141 | Update = True 142 | 143 | Exit Function 144 | End If 145 | Next updateRow 146 | End Function 147 | 148 | 149 | Private Sub SetRowValues(row As Long, values) 150 | For Each column In m_columns 151 | If values.Exists(column.name) Then 152 | Dim insertionCell 153 | Set insertionCell = m_sheet.Cells(row, column.index) 154 | 155 | insertionCell.value = values(column.name) 156 | End If 157 | Next column 158 | End Sub 159 | 160 | 161 | Public Sub Initialize(worksheet) 162 | m_primaryKey = -1 163 | 164 | Set m_sheet = worksheet 165 | Set m_columns = New Collection 166 | 167 | Dim firstRow 168 | Set firstRow = m_sheet.Rows(1) 169 | 170 | Dim i As Integer 171 | For i = 1 To firstRow.Cells.count 172 | Dim column 173 | Set column = firstRow.Cells(i) 174 | 175 | Dim text As String 176 | text = Trim(column.text) 177 | 178 | If text = "" Then 179 | Exit For 180 | End If 181 | 182 | Dim header As WorksheetTableColumnHeader 183 | Set header = New WorksheetTableColumnHeader 184 | header.name = text 185 | header.index = i 186 | 187 | m_columns.Add header 188 | 189 | If column.Font.Bold And m_primaryKey = -1 Then 190 | m_primaryKey = i 191 | End If 192 | Next i 193 | 194 | If m_primaryKey = -1 Then 195 | Err.Raise StatusCode.ErrorNoPrimaryKeyDefined 196 | End If 197 | End Sub 198 | -------------------------------------------------------------------------------- /src/Classes/WorkbookWebController.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "WorkbookWebController" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Implements IWebController 11 | 12 | 13 | Private Enum EntityRequestMode 14 | All 15 | ByKey 16 | Invalid 17 | End Enum 18 | 19 | 20 | Private Type EntityRequest 21 | Mode As EntityRequestMode 22 | Sheet As String 23 | key As String 24 | End Type 25 | 26 | 27 | Private Const UrlPrefix = "/workbook/" 28 | 29 | Private m_wrm As WorksheetRelationshipMapper 30 | Private m_jsonParser As JsonParser 31 | 32 | 33 | Private Sub Class_Initialize() 34 | Set m_wrm = New WorksheetRelationshipMapper 35 | Set m_jsonParser = New JsonParser 36 | End Sub 37 | 38 | 39 | Private Function IWebController_MatchesUrl(requestUrl As String) As Boolean 40 | IWebController_MatchesUrl = requestUrl Like UrlPrefix & "*" 41 | End Function 42 | 43 | 44 | Private Function IWebController_ProcessRequest(request As HttpRequest) As HttpResponse 45 | Dim requestHandled As Boolean 46 | requestHandled = False 47 | 48 | Dim response As HttpResponse 49 | Set response = New HttpResponse 50 | 51 | response.Headers.AddHeader "Content-Type", "application/json" 52 | 53 | Dim parsedRequest As EntityRequest 54 | parsedRequest = ParseRequestUrl(request.Url) 55 | 56 | Dim isJsonRequest As Boolean 57 | isJsonRequest = RequestContainsJsonData(request) 58 | 59 | If parsedRequest.Mode = All Then 60 | If request.RequestMethod = MethodGet Then 61 | Dim values As IJson 62 | Set values = m_wrm.All(parsedRequest.Sheet) 63 | 64 | response.StatusCode = 200 65 | response.body = values.ToJson() 66 | 67 | requestHandled = True 68 | ElseIf request.RequestMethod = MethodPost And isJsonRequest Then 69 | Dim postBody As JsonObject 70 | Set postBody = m_jsonParser.ParseObject(request.body) 71 | 72 | m_wrm.Insert parsedRequest.Sheet, postBody 73 | 74 | Dim responseValue As IJson 75 | Set responseValue = postBody 76 | 77 | Dim insertedId As JsonValue 78 | Set insertedId = postBody.GetProperty(m_wrm.PrimaryKeyName(parsedRequest.Sheet)) 79 | 80 | response.Headers.AddHeader "Location", UrlPrefix & parsedRequest.Sheet & "/" & insertedId.value 81 | response.StatusCode = 201 82 | response.body = responseValue.ToJson() 83 | 84 | requestHandled = True 85 | End If 86 | ElseIf parsedRequest.Mode = ByKey Then 87 | If request.RequestMethod = MethodGet Then 88 | Dim getValue As IJson 89 | Set getValue = m_wrm.Find(parsedRequest.Sheet, parsedRequest.key) 90 | 91 | response.body = getValue.ToJson() 92 | 93 | ' this is a null value check in disguise 94 | If TypeName(getValue) = "JsonValue" Then 95 | response.StatusCode = 404 96 | Else ' type is JsonObject 97 | response.StatusCode = 200 98 | End If 99 | 100 | requestHandled = True 101 | ElseIf request.RequestMethod = MethodDelete Then 102 | Dim deleteValue As IJson 103 | Set deleteValue = m_wrm.Delete(parsedRequest.Sheet, parsedRequest.key) 104 | 105 | If deleteValue Is Nothing Then 106 | response.StatusCode = 404 107 | Else 108 | response.StatusCode = 200 109 | response.Headers.AddHeader "Content-Type", "application/json" 110 | response.body = deleteValue.ToJson() 111 | End If 112 | 113 | requestHandled = True 114 | ElseIf request.RequestMethod = MethodPut And isJsonRequest Then 115 | Dim putBody As JsonObject 116 | Set putBody = m_jsonParser.ParseObject(request.body) 117 | 118 | If m_wrm.Update(parsedRequest.Sheet, parsedRequest.key, putBody) Then 119 | response.StatusCode = 204 120 | Else 121 | response.StatusCode = 404 122 | End If 123 | 124 | requestHandled = True 125 | End If 126 | End If 127 | 128 | If Not requestHandled Then 129 | response.StatusCode = 404 130 | End If 131 | 132 | Set IWebController_ProcessRequest = response 133 | End Function 134 | 135 | 136 | Private Function ParseRequestUrl(requestUrl As String) As EntityRequest 137 | requestUrl = StringExtensions.Substring(requestUrl, Len(UrlPrefix)) 138 | 139 | Dim urlParts 140 | urlParts = Split(requestUrl, "/") 141 | 142 | Dim partCount As Integer 143 | partCount = UBound(urlParts) + 1 144 | 145 | If partCount > 2 Then 146 | ParseRequestUrl.Mode = Invalid 147 | Exit Function 148 | End If 149 | 150 | ParseRequestUrl.Sheet = urlParts(0) 151 | 152 | If partCount = 1 Then 153 | ParseRequestUrl.Mode = All 154 | Exit Function 155 | End If 156 | 157 | ParseRequestUrl.key = Trim(urlParts(1)) 158 | 159 | If Len(ParseRequestUrl.key) = 0 Then 160 | ParseRequestUrl.Mode = All 161 | Else 162 | ParseRequestUrl.Mode = ByKey 163 | End If 164 | End Function 165 | 166 | 167 | Private Function RequestContainsJsonData(request As HttpRequest) As Boolean 168 | RequestContainsJsonData = False 169 | 170 | For Each header In request.Headers.GetEnumerator 171 | Dim n As String 172 | n = LCase(header.name) 173 | 174 | Dim v As String 175 | v = LCase(header.value) 176 | 177 | If LCase(header.name) = "content-type" And (LCase(header.value) = "application/json" Or LCase(header.value) = "application/json;charset=utf-8") Then 178 | RequestContainsJsonData = True 179 | Exit Function 180 | End If 181 | Next header 182 | End Function 183 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![webxcel logo](images/logo.png) 2 | 3 | [![license](https://img.shields.io/github/license/michaelneu/webxcel.svg)](https://github.com/michaelneu/webxcel) 4 | [![GitHub tag](https://img.shields.io/github/tag/michaelneu/webxcel.svg)](https://github.com/michaelneu/webxcel) 5 | [![Github All Releases](https://img.shields.io/github/downloads/michaelneu/webxcel/total.svg)](https://github.com/michaelneu/webxcel) 6 | [![GitHub issues](https://img.shields.io/github/issues/michaelneu/webxcel.svg)](https://github.com/michaelneu/webxcel) 7 | [![GitHub pull requests](https://img.shields.io/github/issues-pr/michaelneu/webxcel.svg)](https://github.com/michaelneu/webxcel) 8 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fmichaelneu%2Fwebxcel.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fmichaelneu%2Fwebxcel?ref=badge_shield) 9 | 10 | Webxcel creates a full-fledged RESTful web backend from your Microsoft Excel workbooks. It is written in 100% plain Visual Basic macros and comes with a lot of handy tools to help you build the next big thing. 11 | 12 | 13 | ## Features 14 | 15 | #### Rapid prototyping 16 | 17 | Build your web applications using the autogenerated RESTful CRUD endpoints and the static file server. Webxcel even supports mapping worksheet relationships, so you can model your table schema as flexible as you wish. 18 | 19 | To configure a new table, simply insert your column names in the first row of an empty sheet and make the cell of your primary key **bold**. References to other tables can be introduced using `fk_` columns and foreign keys: 20 | 21 | ![worksheet relationship mapping](images/wrm-schema.png) 22 | 23 | When accessing `GET /workbook/cities`, webxcel will return a fully mapped JSON object (response formatted for better readability): 24 | 25 | ```http 26 | HTTP/1.1 200 OK 27 | Content-Type: application/json 28 | Server: Microsoft Excel/16.0 29 | Content-Length: 200 30 | Connection: close 31 | 32 | [ 33 | { 34 | "id": "1", 35 | "city": "Seattle", 36 | "states": { 37 | "short_name": "WA", 38 | "full_name": "Washington" 39 | } 40 | }, 41 | { 42 | "id": "2", 43 | "city": "Springfield", 44 | "states": null 45 | } 46 | ] 47 | ``` 48 | 49 | 50 | #### Batteries already included 51 | 52 | No need to spin up a cloud server or function-as-a-service provider, implement rich server side logic right in Microsoft Excel using the built-in tools you already know and love. Using Microsoft Excel's immersive charting, you can gather even deeper insights from your data. 53 | 54 | 55 | #### Scalable 56 | 57 | Webxcel supports scaling from a mere 10% to 400% for extreme detailed data insights. This makes it a perfect fit for small startups to global corporations. By default, webxcel projects are scaled 100%, but you can adjust this setting in the lower right corner of Microsoft Excel to fit your needs. 58 | 59 | 60 | #### Ready for deployment 61 | 62 | Many new devices come with Microsoft Office preinstalled, most of the time you won't have to do any setup at all. Deploying a project is as easy as dropping the file on the server and starting webxcel. Also backup is a no-brainer, simply copy and paste the project file to your backup location (e.g. a flash drive) and you're all set. 63 | 64 | 65 | #### Future proof with compatibility in mind 66 | 67 | Webxcel is built on Windows Sockets 2 and runs on any Microsoft Excel version starting from Microsoft Office 2007, but should work on any macro enabled setup. 68 | 69 | 70 | #### Hassle-free PHP 71 | 72 | Everybody hates PHP configurations. That's why webxcel ships with a PHP plugin that just works, no configuration needed. It's like serverless but better! 73 | 74 | 75 | #### Missing something? 76 | 77 | Check out the [features project](https://github.com/michaelneu/webxcel/projects/2) to get the latest news and ideas for webxcel, or file a [new issue](https://github.com/michaelneu/webxcel/issues/new). 78 | 79 | 80 | ## Getting started 81 | 82 | To quickstart development, check out the [example folder](example) for a simple todo app using React and webxcel. 83 | 84 | 85 | #### Creating new projects 86 | 87 | The `build.ps1` PowerShell script creates an empty webxcel project in `build/webxcel.xlsm`, which you can alter to create your table schema. You can also import the classes and modules by hand, but this will consume considerably more time since the import dialog only allows you to select one file at a time. 88 | 89 | In order for the build script to succeed, you may have to make a few changes to your Excel settings. Open the Trust Center in the options menu, select Macro Settings and check the following options: 90 | - Enable all macros 91 | - Trust access to the VBA project object model 92 | 93 | If you can't run the build script at all, use the following command to temporarily enable PowerShell scripts for the duration of your [PowerShell session](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.security/set-executionpolicy?view=powershell-6#notes): 94 | ```powershell 95 | Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope Process 96 | ``` 97 | 98 | #### Running webxcel 99 | 100 | Webxcel can be started using either the `Main` sub through the developer ribbon or by assigning it to an interactive component like a button. Once started, webxcel will serve all static files located in the same directory as your project's `.xlsm`. 101 | 102 | During webxcel's runtime, Microsoft Excel will not respond. This is due to Visual Basic being executed on the main UI thread. As this is a known issue, webxcel creates a lockfile `.xlsm.lock`, which you can delete to gracefully shut down the server. You also could force-quit Microsoft Excel, but this might result in the port being blocked from further usage until a reboot (webxcel will raise the error [`ErrorSocketBind`](src/Modules/StatusCode.bas) whenever it can't bind to a specific port). 103 | 104 | 105 | ## Contributing 106 | 107 | To contribute, clone the repository, build an empty webxcel project and start hacking in the Visual Basic editor (Alt + F11). Once you've finished your contribution, export your classes or modules and create a [pull request](https://github.com/michaelneu/webxcel/compare). As Visual Basic is case-insensitive, please check your exported classes to minimize renaming commits (e.g. after introducing a new variable `Dim name` and the editor globally renamed `Name` to `name`). 108 | 109 | 110 | ## Is it any good? 111 | 112 | [Yes](https://news.ycombinator.com/item?id=3067434). 113 | 114 | 115 | ## License 116 | 117 | Webxcel is released under the [MIT license](LICENSE). 118 | -------------------------------------------------------------------------------- /src/Classes/JsonParser.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "JsonParser" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Private m_null As JsonValue 11 | 12 | 13 | Private Sub Class_Initialize() 14 | Set m_null = New JsonValue 15 | m_null.value = Null 16 | End Sub 17 | 18 | 19 | Private Function ConsumeObject(ByVal text As String, ByRef counter As Long) As String 20 | Dim buffer As String 21 | Dim openBrackets As Long 22 | Dim openBraces As Long 23 | 24 | Dim inString As Boolean 25 | inString = False 26 | 27 | For counter = counter To Len(text) 28 | Dim char As String 29 | char = StringExtensions.CharAt(text, counter) 30 | 31 | buffer = buffer & char 32 | 33 | If char = """" Then 34 | inString = Not inString 35 | End If 36 | 37 | If Not inString Then 38 | Select Case char 39 | Case "[" 40 | openBrackets = openBrackets + 1 41 | 42 | Case "]" 43 | openBrackets = openBrackets - 1 44 | 45 | Case "{" 46 | openBraces = openBraces + 1 47 | 48 | Case "}" 49 | openBraces = openBraces - 1 50 | End Select 51 | End If 52 | 53 | If openBrackets = 0 And openBraces = 0 Then 54 | ConsumeObject = buffer 55 | Exit Function 56 | End If 57 | Next counter 58 | 59 | Err.Raise StatusCode.ErrorMalformedJson 60 | End Function 61 | 62 | 63 | Public Function ParseObject(obj As String) As JsonObject 64 | If Not StringExtensions.StartsWith(obj, "{") Or Not StringExtensions.EndsWith(obj, "}") Then 65 | Err.Raise StatusCode.ErrorMalformedJson 66 | Exit Function 67 | End If 68 | 69 | Set ParseObject = New JsonObject 70 | 71 | obj = StringExtensions.Substring(obj, 1) 72 | obj = Left(obj, Len(obj) - 1) 73 | 74 | Dim propertyName As String 75 | Dim value As IJson 76 | Dim valueBuffer As String 77 | Dim i As Long 78 | 79 | For i = 1 To Len(obj) 80 | Dim char As String 81 | char = StringExtensions.CharAt(obj, i) 82 | 83 | Select Case char 84 | Case """" 85 | If Len(propertyName) > 0 Then 86 | Set value = ParseString(ConsumeString(obj, i)) 87 | 88 | ParseObject.SetProperty propertyName, value 89 | propertyName = "" 90 | Else 91 | propertyName = ParseString(ConsumeString(obj, i)).value 92 | End If 93 | 94 | Case ":" 95 | If Len(propertyName) = 0 Then 96 | Err.Raise StatusCode.ErrorMalformedJson 97 | Exit Function 98 | End If 99 | 100 | Case "[" 101 | Set value = ParseArray(ConsumeArray(obj, i)) 102 | 103 | ParseObject.SetProperty propertyName, value 104 | propertyName = "" 105 | 106 | Case "{" 107 | Set value = ParseObject(ConsumeObject(obj, i)) 108 | 109 | ParseObject.SetProperty propertyName, value 110 | propertyName = "" 111 | 112 | Case " ", vbTab, vbCr, vbLf, vbCrLf 113 | If Len(valueBuffer) > 0 And Len(propertyName) = 0 Then 114 | Err.Raise StatusCode.ErrorMalformedJson 115 | Exit Function 116 | End If 117 | 118 | Case "," 119 | If Len(valueBuffer) > 0 Then 120 | If Len(propertyName) = 0 Then 121 | Err.Raise StatusCode.ErrorMalformedJson 122 | Exit Function 123 | End If 124 | 125 | Set value = ParsePlainText(valueBuffer) 126 | ParseObject.SetProperty propertyName, value 127 | 128 | propertyName = "" 129 | valueBuffer = "" 130 | End If 131 | 132 | Case Else 133 | valueBuffer = valueBuffer & char 134 | End Select 135 | Next i 136 | 137 | If Len(valueBuffer) > 0 Then 138 | If Len(propertyName) = 0 Then 139 | Err.Raise StatusCode.ErrorMalformedJson 140 | Exit Function 141 | End If 142 | 143 | Set value = ParsePlainText(valueBuffer) 144 | ParseObject.SetProperty propertyName, value 145 | End If 146 | End Function 147 | 148 | 149 | Private Function ConsumeString(ByVal text As String, ByRef counter As Long) As String 150 | Dim buffer As String 151 | buffer = "" 152 | 153 | For counter = counter To Len(text) 154 | Dim char As String 155 | char = StringExtensions.CharAt(text, counter) 156 | 157 | buffer = buffer & char 158 | 159 | If char = """" And Len(buffer) > 1 Then 160 | ConsumeString = buffer 161 | Exit Function 162 | End If 163 | Next counter 164 | 165 | Err.Raise StatusCode.ErrorMalformedJson 166 | End Function 167 | 168 | 169 | Public Function ParseString(str As String) As JsonValue 170 | str = StringExtensions.Substring(str, 1) 171 | str = Left(str, Len(str) - 1) 172 | 173 | Set ParseString = New JsonValue 174 | ParseString.value = str 175 | End Function 176 | 177 | 178 | Private Function ConsumeArray(ByVal text As String, ByRef counter As Long) As String 179 | Dim buffer As String 180 | Dim openBrackets As Long 181 | Dim openBraces As Long 182 | 183 | Dim inString As Boolean 184 | inString = False 185 | 186 | For counter = counter To Len(text) 187 | Dim char As String 188 | char = StringExtensions.CharAt(text, counter) 189 | 190 | buffer = buffer & char 191 | 192 | If char = """" Then 193 | inString = Not inString 194 | End If 195 | 196 | If Not inString Then 197 | Select Case char 198 | Case "[" 199 | openBrackets = openBrackets + 1 200 | 201 | Case "]" 202 | openBrackets = openBrackets - 1 203 | 204 | Case "{" 205 | openBraces = openBraces + 1 206 | 207 | Case "}" 208 | openBraces = openBraces - 1 209 | End Select 210 | End If 211 | 212 | If openBrackets = 0 And openBraces = 0 Then 213 | ConsumeArray = buffer 214 | Exit Function 215 | End If 216 | Next counter 217 | 218 | Err.Raise StatusCode.ErrorMalformedJson 219 | End Function 220 | 221 | 222 | Public Function ParseArray(arr As String) As JsonArray 223 | If Not StringExtensions.StartsWith(arr, "[") Or Not StringExtensions.EndsWith(arr, "]") Then 224 | Err.Raise StatusCode.ErrorMalformedJson 225 | Exit Function 226 | End If 227 | 228 | Set ParseArray = New JsonArray 229 | 230 | arr = StringExtensions.Substring(arr, 1) 231 | arr = Left(arr, Len(arr) - 1) 232 | 233 | Dim item As IJson 234 | Dim itemBuffer As String 235 | Dim i As Long 236 | 237 | For i = 1 To Len(arr) 238 | Dim char As String 239 | char = StringExtensions.CharAt(arr, i) 240 | 241 | Select Case char 242 | Case """" 243 | Set item = ParseString(ConsumeString(arr, i)) 244 | ParseArray.AddItem item 245 | 246 | Case "[" 247 | Set item = ParseArray(ConsumeArray(arr, i)) 248 | ParseArray.AddItem item 249 | 250 | Case "{" 251 | Set item = ParseObject(ConsumeObject(arr, i)) 252 | ParseArray.AddItem item 253 | 254 | Case " ", vbTab, vbCr, vbLf, vbCrLf 255 | If Len(valueBuffer) > 0 Then 256 | Err.Raise StatusCode.ErrorMalformedJson 257 | Exit Function 258 | End If 259 | 260 | Case "," 261 | If Len(itemBuffer) > 0 Then 262 | Set item = ParsePlainText(itemBuffer) 263 | ParseArray.AddItem item 264 | 265 | itemBuffer = "" 266 | End If 267 | 268 | Case Else 269 | itemBuffer = itemBuffer & char 270 | End Select 271 | Next i 272 | 273 | If Len(itemBuffer) > 0 Then 274 | Set item = ParsePlainText(itemBuffer) 275 | ParseArray.AddItem item 276 | End If 277 | End Function 278 | 279 | 280 | Private Function ParsePlainText(text As String) As JsonValue 281 | If LCase(text) = "null" Then 282 | Set ParsePlainText = m_null 283 | Else 284 | Dim textAsJsonValue As JsonValue 285 | Set textAsJsonValue = New JsonValue 286 | textAsJsonValue.value = text 287 | 288 | Set ParsePlainText = textAsJsonValue 289 | End If 290 | End Function 291 | -------------------------------------------------------------------------------- /src/Classes/WorksheetRelationshipMapper.cls: -------------------------------------------------------------------------------- 1 | VERSION 1.0 CLASS 2 | BEGIN 3 | MultiUse = -1 'True 4 | END 5 | Attribute VB_Name = "WorksheetRelationshipMapper" 6 | Attribute VB_GlobalNameSpace = False 7 | Attribute VB_Creatable = False 8 | Attribute VB_PredeclaredId = False 9 | Attribute VB_Exposed = False 10 | Private m_tables As WorksheetTableCollection 11 | 12 | 13 | Private Sub Class_Initialize() 14 | Set m_tables = New WorksheetTableCollection 15 | End Sub 16 | 17 | 18 | Public Function PrimaryKeyName(sheetName As String) As String 19 | Dim table As WorksheetTable 20 | Set table = m_tables.FindTable(sheetName) 21 | 22 | PrimaryKeyName = table.PrimaryKey 23 | End Function 24 | 25 | 26 | Public Function All(sheetName As String) As JsonArray 27 | Set All = New JsonArray 28 | 29 | Dim table As WorksheetTable 30 | Set table = m_tables.FindTable(sheetName) 31 | 32 | For Each entry In table.Entries 33 | Dim obj As JsonObject 34 | Set obj = New JsonObject 35 | 36 | For Each column In table.Columns 37 | Dim val As JsonValue 38 | Set val = New JsonValue 39 | 40 | val.value = entry(column) 41 | 42 | obj.SetProperty column, val 43 | Next column 44 | 45 | All.AddItem MapEntryRelationships(obj) 46 | Next entry 47 | End Function 48 | 49 | 50 | Private Function MapEntryRelationships(entry As JsonObject) As JsonObject 51 | Set MapEntryRelationships = entry 52 | 53 | For Each property In entry.PropertyNames 54 | Dim val As JsonValue 55 | Set val = entry.GetProperty(property) 56 | 57 | If property Like "fk_*" Then 58 | Dim foreignTableName As String 59 | foreignTableName = StringExtensions.Substring(property, 3) 60 | 61 | Dim newVal As IJson 62 | 63 | If val.ContainsNull Then 64 | Set newVal = val 65 | Else 66 | Dim foreignObject As JsonObject 67 | Set foreignObject = New JsonObject 68 | 69 | Dim foreignTable As WorksheetTable 70 | Set foreignTable = m_tables.FindTable(foreignTableName) 71 | 72 | For Each foreignEntry In foreignTable.Entries 73 | If foreignEntry(foreignTable.PrimaryKey) = val.value Then 74 | Set foreignObject = New JsonObject 75 | 76 | For Each foreignColumn In foreignTable.Columns 77 | Dim foreignVal As JsonValue 78 | Set foreignVal = New JsonValue 79 | foreignVal.value = foreignEntry(foreignColumn) 80 | 81 | foreignObject.SetProperty foreignColumn, foreignVal 82 | Next foreignColumn 83 | 84 | Set foreignObject = MapEntryRelationships(foreignObject) 85 | End If 86 | Next foreignEntry 87 | 88 | If foreignObject Is Nothing Then 89 | Err.Raise StatusCode.ErrorInvalidForeignKeyUsed 90 | End If 91 | 92 | Set newVal = foreignObject 93 | End If 94 | 95 | entry.RemoveProperty property 96 | entry.SetProperty foreignTableName, newVal 97 | End If 98 | Next property 99 | End Function 100 | 101 | 102 | Public Function Find(sheetName As String, key) As IJson 103 | Dim table As WorksheetTable 104 | Set table = m_tables.FindTable(sheetName) 105 | 106 | Dim allValues As JsonArray 107 | Set allValues = All(sheetName) 108 | 109 | Dim i As Integer 110 | For i = 0 To allValues.count - 1 111 | Dim value As JsonObject 112 | Set value = allValues.GetItem(i) 113 | 114 | Dim valueAsJsonValue As JsonValue 115 | Set valueAsJsonValue = value.GetProperty(table.PrimaryKey) 116 | 117 | If valueAsJsonValue.value = key Then 118 | Set Find = value 119 | Exit Function 120 | End If 121 | Next i 122 | 123 | Dim nullValue As JsonValue 124 | Set nullValue = New JsonValue 125 | nullValue.value = Null 126 | 127 | Set Find = nullValue 128 | End Function 129 | 130 | 131 | Public Sub Insert(sheetName As String, item As JsonObject) 132 | Dim table As WorksheetTable 133 | Set table = m_tables.FindTable(sheetName) 134 | 135 | Dim itemToInsert 136 | Set itemToInsert = CreateObject("Scripting.Dictionary") 137 | 138 | Dim pkFound As Boolean 139 | pkFound = False 140 | 141 | For Each property In item.PropertyNames 142 | If property = table.PrimaryKey Then 143 | pkFound = True 144 | End If 145 | 146 | Dim propertyIsForeignKey As Boolean 147 | propertyIsForeignKey = False 148 | 149 | For Each column In table.Columns 150 | If "fk_" & property = column Then 151 | propertyIsForeignKey = True 152 | Exit For 153 | End If 154 | Next column 155 | 156 | If propertyIsForeignKey Then 157 | Dim foreignPropertyName As String 158 | foreignPropertyName = "fk_" & property 159 | 160 | Dim foreignTableName As String 161 | foreignTableName = property 162 | 163 | Dim foreignTable As WorksheetTable 164 | Set foreignTable = m_tables.FindTable(foreignTableName) 165 | 166 | Dim foreignItem As IJson 167 | Set foreignItem = item.GetProperty(property) 168 | 169 | If TypeName(foreignItem) = "JsonValue" Then 170 | Dim foreignItemAsValue As JsonValue 171 | Set foreignItemAsValue = foreignItem 172 | 173 | If foreignItemAsValue.ContainsNull Then 174 | itemToInsert.Add foreignPropertyName, "" 175 | Else 176 | Err.Raise StatusCode.ErrorInvalidForeignKeyUsed 177 | End If 178 | ElseIf TypeName(foreignItem) = "JsonObject" Then 179 | Dim foreignItemAsObject As JsonObject 180 | Set foreignItemAsObject = foreignItem 181 | 182 | Dim foreignItemContainsPk As Boolean 183 | foreignItemContainsPk = False 184 | 185 | For Each foreignProperty In foreignItemAsObject.PropertyNames 186 | If foreignProperty = foreignTable.PrimaryKey Then 187 | Dim foreignPkValue As JsonValue 188 | Set foreignPkValue = foreignItemAsObject.GetProperty(foreignProperty) 189 | 190 | itemToInsert.Add foreignPropertyName, foreignPkValue.value 191 | foreignItemContainsPk = True 192 | End If 193 | Next foreignProperty 194 | 195 | If foreignItemContainsPk Then 196 | Insert foreignTableName, foreignItemAsObject 197 | Else 198 | Err.Raise StatusCode.ErrorInvalidForeignKeyUsed 199 | End If 200 | Else 201 | Err.Raise StatusCode.ErrorInvalidForeignKeyUsed 202 | End If 203 | Else 204 | Dim value As JsonValue 205 | Set value = item.GetProperty(property) 206 | 207 | If value.ContainsNull Then 208 | itemToInsert.Add property, "" 209 | Else 210 | itemToInsert.Add property, value.value & "" 211 | End If 212 | End If 213 | Next property 214 | 215 | If Not pkFound Then 216 | Err.Raise StatusCode.ErrorDataDoesntContainPrimaryKey 217 | Exit Sub 218 | End If 219 | 220 | table.Insert itemToInsert 221 | End Sub 222 | 223 | 224 | Public Function Delete(sheetName As String, key As String) As JsonObject 225 | Dim table As WorksheetTable 226 | Set table = m_tables.FindTable(sheetName) 227 | 228 | Dim entry 229 | Set entry = table.Delete(key) 230 | 231 | If Not entry Is Nothing Then 232 | Dim deletedValue As JsonObject 233 | Set deletedValue = New JsonObject 234 | 235 | For Each column In table.Columns 236 | Dim val As JsonValue 237 | Set val = New JsonValue 238 | 239 | val.value = entry(column) 240 | 241 | deletedValue.SetProperty column, val 242 | Next column 243 | 244 | Set Delete = MapEntryRelationships(deletedValue) 245 | End If 246 | End Function 247 | 248 | 249 | Public Function Update(sheetName As String, key As String, item As JsonObject) As Boolean 250 | Dim table As WorksheetTable 251 | Set table = m_tables.FindTable(sheetName) 252 | 253 | Dim values 254 | Set values = CreateObject("Scripting.Dictionary") 255 | 256 | For Each property In item.PropertyNames 257 | Dim val As JsonValue 258 | Set val = item.GetProperty(property) 259 | 260 | If IsNull(val.value) Then 261 | values(property) = "" 262 | Else 263 | values(property) = val.value 264 | End If 265 | Next property 266 | 267 | Update = table.Update(key, values) 268 | End Function 269 | --------------------------------------------------------------------------------