Skip to content
This repository has been archived by the owner on Jul 31, 2024. It is now read-only.

Commit

Permalink
Merge pull request #187 from Sleitnick/separated-middleware
Browse files Browse the repository at this point in the history
Separated middleware
  • Loading branch information
Sleitnick authored Dec 25, 2021
2 parents 1bf4158 + c670dbe commit 8ffd06f
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 78 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## 1.4.0

- Add ability to set independent middleware per service, but on the server and the client
- Added tutorial video links
- Add short-circuit evaluation to `GetService` and `GetController` functions for better performance when the service/controller exists
- Change Comm module to use service name as namespace instead of nested `__comm__` folder
- Documentation improvements
- Breaking changes to middleware assignment (now within one `Middleware` table instead of two for inbound/outbound)

## 1.3.0

- Add support for RemoteProperties via Comm library
Expand Down
47 changes: 38 additions & 9 deletions docs/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@ Knit's networking layer uses the [Comm](https://sleitnick.github.io/RbxUtil/api/

Middleware can be used to both transform inbound/outbound arguments, and also decide to drop requests/responses. This is useful for many use-cases, such as automatically serializing/deserializing complex data types over the network, or sanitizing incoming data.

Middleware can be added on both the server and client, and affects functions and signals.

As of right now, middleware is only at a global level across Knit. In the future, it will be possible to have custom middleware per service and controller.
Middleware can be added on both the server and client, and affects functions and signals. Middleware can either be added at the Knit global level, or per service.

## Usage

Middleware is added when Knit is started: `Knit.Start({InboundMiddleware: {...}, OutboundMiddleware: {...}})`. Each "middleware" item in the tables is a function. On the client, this function takes an array table containing all the arguments passed along. On the server, it is nearly the same, except the first argument before the arguments table is the player.
Middleware is added when Knit is started: `Knit.Start({Middleware = {Inbound = {...}, Outbound = {...}}})` _or_ on each service. Each "middleware" item in the tables is a function. On the client, this function takes an array table containing all the arguments passed along. On the server, it is nearly the same, except the first argument before the arguments table is the player.

Each function should return a boolean, indicating whether or not to continue to the request/response. If `false`, an optional variadic list of items can be returned, which will be returned back to the caller (essentially a short-circuit, but still returning data).

Expand All @@ -33,7 +31,7 @@ local function Logger(args: {any})
end

Knit.Start({
InboundMiddleware = {Logger}
Middleware = {Inbound = {Logger}}
})
```

Expand All @@ -45,7 +43,7 @@ local function Logger(player: Player, args: {any})
end

Knit.Start({
InboundMiddleware = {Logger}
Middleware = {Inbound = {Logger}}
})
```

Expand All @@ -62,7 +60,36 @@ local function DoubleNumbers(args)
return true
end

Knit.Start({InboundMiddleware = {DoubleNumbers}})
Knit.Start({Middleware = {Inbound = {DoubleNumbers}}})
```

#### Per-Service Example

Middleware can also be targeted per-service, which will override the global level middleware for the given service.
```lua
-- Server-side:
local MyService = Knit.CreateService {
Name = "MyService",
Client = {},
Middleware = {
Inbound = {Logger},
Outbound = {},
},
}
```

On the client, things look a little different. Middleware is still per-service, not controller, so the definitions of per-service middleware need to go within `Knit.Start()` on the client:
```lua
-- Client-side:
Knit.Start({
PerServiceMiddleware = {
-- Mapped by name of the service
MyService = {
Inbound = {Logger},
Outbound = {},
},
},
})
```

#### Serialization
Expand Down Expand Up @@ -113,7 +140,9 @@ local function OutboundClass(args)
end

Knit.Start({
InboundMiddleware = {InboundClass},
OutboundMiddleware = {OutboundClass}
Middleware = {
Inbound = {InboundClass},
Outbound = {OutboundClass},
},
})
```
71 changes: 62 additions & 9 deletions roblox.toml

Large diffs are not rendered by default.

98 changes: 71 additions & 27 deletions src/KnitClient.lua
Original file line number Diff line number Diff line change
@@ -1,8 +1,41 @@
--[=[
@interface Middleware
.Inbound ClientMiddleware?
.Outbound ClientMiddleware?
@within KnitClient
]=]
type Middleware = {
Inbound: ClientMiddleware?,
Outbound: ClientMiddleware?,
}

--[=[
@type ClientMiddlewareFn (args: {any}) -> (shouldContinue: boolean, ...: any)
@within KnitClient
For more info, see [ClientComm](https://sleitnick.github.io/RbxUtil/api/ClientComm/) documentation.
]=]
type ClientMiddlewareFn = (args: {any}) -> (boolean, ...any)

--[=[
@type ClientMiddleware {ClientMiddlewareFn}
@within KnitClient
An array of client middleware functions.
]=]
type ClientMiddleware = {ClientMiddlewareFn}

--[=[
@type PerServiceMiddleware {[string]: Middleware}
@within KnitClient
]=]
type PerServiceMiddleware = {[string]: Middleware}

--[=[
@interface ControllerDef
.Name string
.[any] any
@within KnitClient
Used to define a controller when creating it in `CreateController`.
]=]
type ControllerDef = {
Name: string,
Expand All @@ -29,33 +62,27 @@ type Service = {
[any]: any,
}

--[=[
@type ClientMiddlewareFn (args: {any}) -> (shouldContinue: boolean, ...: any)
@within KnitClient
For more info, see [ClientComm](https://sleitnick.github.io/RbxUtil/api/ClientComm/) documentation.
]=]

--[=[
@interface KnitOptions
.ServicePromises boolean?
.InboundMiddleware ClientMiddlewareFn?
.OutboundMiddleware ClientMiddlewareFn?
.Middleware Middleware?
.PerServiceMiddleware PerServiceMiddleware?
@within KnitClient
- `ServicePromises` defaults to `true` and indicates if service methods use promises.
- `InboundMiddleware` and `OutboundMiddleware` default to `nil`.
- Each service will go through the defined middleware, unless the service
has middleware defined in `PerServiceMiddleware`.
]=]
type KnitOptions = {
ServicePromises: boolean,
InboundMiddleware: {(...any) -> (boolean, ...any)}?,
OutboundMiddleware: {(...any) -> (boolean, ...any)}?,
Middleware: Middleware?,
PerServiceMiddleware: PerServiceMiddleware?
}

local defaultOptions: KnitOptions = {
ServicePromises = true;
InboundMiddleware = nil;
OutboundMiddleware = nil;
Middleware = nil;
PerServiceMiddleware = {};
}

local selectedOptions = nil
Expand Down Expand Up @@ -99,13 +126,6 @@ local startedComplete = false
local onStartedComplete = Instance.new("BindableEvent")


local function BuildService(serviceName: string, folder: Instance): Service
local service = ClientComm.new(folder, selectedOptions.ServicePromises):BuildObject(selectedOptions.InboundMiddleware, selectedOptions.OutboundMiddleware)
services[serviceName] = service
return service
end


local function DoesControllerExist(controllerName: string): boolean
local controller: Controller? = controllers[controllerName]
return controller ~= nil
Expand All @@ -120,6 +140,23 @@ local function GetServicesFolder()
end


local function GetMiddlewareForService(serviceName: string)
local knitMiddleware = selectedOptions.Middleware or {}
local serviceMiddleware = selectedOptions.PerServiceMiddleware[serviceName]
return serviceMiddleware or knitMiddleware
end


local function BuildService(serviceName: string)
local folder = GetServicesFolder()
local middleware = GetMiddlewareForService(serviceName)
local clientComm = ClientComm.new(folder, selectedOptions.ServicePromises, serviceName)
local service = clientComm:BuildObject(middleware.Inbound, middleware.Outbound)
services[serviceName] = service
return service
end


--[=[
@param controllerDefinition ControllerDef
@return Controller
Expand Down Expand Up @@ -225,11 +262,13 @@ end
:::
]=]
function KnitClient.GetService(serviceName: string): Service
local service = services[serviceName]
if service then
return service
end
assert(started, "Cannot call GetService until Knit has been started")
assert(type(serviceName) == "string", "ServiceName must be a string; got " .. type(serviceName))
local folder: Instance? = GetServicesFolder():FindFirstChild(serviceName)
assert(folder ~= nil, "Could not find service \"" .. serviceName .. "\". Check the service name and that the service has client-facing methods/RemoteSignals/RemoteProperties.")
return services[serviceName] or BuildService(serviceName, folder :: Instance)
return BuildService(serviceName)
end


Expand All @@ -240,11 +279,13 @@ end
is not found.
]=]
function KnitClient.GetController(controllerName: string): Controller
local controller = controllers[controllerName]
if controller then
return controller
end
assert(started, "Cannot call GetController until Knit has been started")
assert(type(controllerName) == "string", "ControllerName must be a string; got " .. type(controllerName))
local controller = controllers[controllerName]
assert(controller ~= nil, " Could not find controller \"" .. controllerName .. "\". Check to verify a controller with this name exists.")
return controller
error("Could not find controller \"" .. controllerName .. "\". Check to verify a controller with this name exists.", 2)
end


Expand Down Expand Up @@ -285,6 +326,9 @@ function KnitClient.Start(options: KnitOptions?)
end
end
end
if type(selectedOptions.PerServiceMiddleware) ~= "table" then
selectedOptions.PerServiceMiddleware = {}
end

return Promise.new(function(resolve)

Expand Down
Loading

0 comments on commit 8ffd06f

Please sign in to comment.