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 |
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 | 
2 |
3 | [](https://github.com/michaelneu/webxcel)
4 | [](https://github.com/michaelneu/webxcel)
5 | [](https://github.com/michaelneu/webxcel)
6 | [](https://github.com/michaelneu/webxcel)
7 | [](https://github.com/michaelneu/webxcel)
8 | [](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 | 
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 |
--------------------------------------------------------------------------------