diff --git a/.luaurc b/.luaurc index 90ad478..2d84ab4 100644 --- a/.luaurc +++ b/.luaurc @@ -1,13 +1,13 @@ { "languageMode": "strict", "aliases": { - "std": "~/.lute/typedefs/1.0.0/std", - "adpt": "./src/adapter/", + "utils": "./src/utils", + "c": "/home/wiz/projects/nova-projects/nova_core/tests/web/", "core": "./src/core", "lune": "~/.lune/.typedefs/0.10.4/", "test-runner": "./tests/runner", - "c": "/home/wiz/projects/nova-projects/nova_core/tests/web/", - "utils": "./src/utils", + "adpt": "./src/adapter/", + "std": "~/.lute/typedefs/1.0.0/std", "lint": "~/.lute/typedefs/1.0.0/lint", "lute": "~/.lute/typedefs/1.0.0/lute" } diff --git a/src/core/Attribute/RegistryGenerator.luau b/src/core/Attribute/RegistryGenerator.luau index 51d7011..2a2fa59 100644 --- a/src/core/Attribute/RegistryGenerator.luau +++ b/src/core/Attribute/RegistryGenerator.luau @@ -6,7 +6,7 @@ local registry: { [string]: { [string]: (req: any, ...any) -> any } } = {} local DECORATOR_DIRS = { "guards", - -- "validators", + "validators", "interceptors" } @@ -35,7 +35,7 @@ local function loadRegistry(mainDir: string) local modulePath = mainDir .. "/../" .. dirName .. "/" .. name local ok, result = pcall(require, modulePath) - if ok and type(result) == "function" then + if ok and type(result) == "function" or type(result) == "table" then registry[namespace][name] = result print(`\27[32mLoaded {namespace}: {name}\27[0m`) else diff --git a/src/core/Attribute/Validator.luau b/src/core/Attribute/Validator.luau new file mode 100644 index 0000000..3c88317 --- /dev/null +++ b/src/core/Attribute/Validator.luau @@ -0,0 +1,131 @@ +local registry = require("./RegistryGenerator").registry +local Response = require("@utils/Response") +local Types = require("../../types") + +local function validateBody(schema: { [string]: Types.BodyField }, data: { [string]: any }): { string } + local errors = {} + + for field, rules in schema do + local value = data[field] + + if rules.required and value == nil then + table.insert(errors, `{field} is required`) + continue + end + + if value == nil then + continue + end + + if rules.type and type(value) ~= rules.type then + table.insert(errors, `{field} must be a {rules.type}`) + continue + end + + if rules.pattern and not tostring(value):match(rules.pattern) then + table.insert(errors, `{field} does not match expected pattern`) + end + + if type(value) == "number" then + if rules.min and value < rules.min then + table.insert(errors, `{field} must be at least {rules.min}`) + end + if rules.max and value > rules.max then + table.insert(errors, `{field} must be at most {rules.max}`) + end + end + + if type(value) == "string" then + if rules.min and #value < rules.min then + table.insert(errors, `{field} must be at least {rules.min} characters`) + end + if rules.max and #value > rules.max then + table.insert(errors, `{field} must be at most {rules.max} characters`) + end + end + end + + return errors +end + +local function validateString(schema: { [string]: Types.StringField }, data: { [string]: any }): { string } + local errors = {} + + for field, rules in schema do + local value = data[field] + + if rules.required and value == nil then + table.insert(errors, `{field} is required`) + continue + end + + if value == nil then + continue + end + + local str = tostring(value) + + if rules.pattern and not str:match(rules.pattern) then + table.insert(errors, `{field} does not match expected pattern`) + end + + if rules.minLength and #str < rules.minLength then + table.insert(errors, `{field} must be at least {rules.minLength} characters`) + end + + if rules.maxLength and #str > rules.maxLength then + table.insert(errors, `{field} must be at most {rules.maxLength} characters`) + end + end + + return errors +end + +local function Validator(args: { string }) + return function(req: Types.Request, next: Types.Next) + local attribute = registry["Validator"] + + if not attribute then + return next() + end + + for _, arg in ipairs(args) do + local rule = (attribute[arg] :: any) :: Types.ValidatorSchema + + if not rule then + continue + end + + local errors = {} + + if rule.body then + for _, err in validateBody(rule.body, req.body :: any or {}) do + table.insert(errors, err) + end + end + + if rule.params then + for _, err in validateString(rule.params, req.params or {}) do + table.insert(errors, err) + end + end + + if rule.query then + for _, err in validateString(rule.query, req.query or {}) do + table.insert(errors, err) + end + end + + if #errors > 0 then + return Response.json({ + message = errors, + error = "Bad Request" + }, { status = 400 }) + end + end + + return next() + end +end + +return Validator diff --git a/src/core/Attribute/init.luau b/src/core/Attribute/init.luau index 29fce2e..888d0ce 100644 --- a/src/core/Attribute/init.luau +++ b/src/core/Attribute/init.luau @@ -1,5 +1,7 @@ +local Types = require("../types") + type Attributes = { - [string]: ({any}) -> (any, any) -> any + [string]: ({any}) -> (Types.Request, Types.Next) -> Types.ResponsePayload? } --[[ @@ -7,5 +9,6 @@ type Attributes = { ]] return { Guard = require("@self/Guard"), - Interceptor = require("@self/Interceptor") + Interceptor = require("@self/Interceptor"), + Validator = require("@self/Validator") } :: Attributes diff --git a/src/core/Routing/Generator.luau b/src/core/Routing/Generator.luau index 692e5bb..b6f45c5 100644 --- a/src/core/Routing/Generator.luau +++ b/src/core/Routing/Generator.luau @@ -32,7 +32,7 @@ end local function applyAttributes(routeModule: any, extractedDecorators: { [string]: { string } }) for _, method in METHOD_NAMES do local handler = routeModule[method] - if type(handler) ~= "function" then + if typeof(handler) ~= "function" then continue end diff --git a/src/types.luau b/src/types.luau index 1ed997e..047c678 100644 --- a/src/types.luau +++ b/src/types.luau @@ -1,6 +1,6 @@ -- TODO: Enhance the types -export type Next = () -> NextResponse +export type Next = () -> ResponsePayload export type Nova = { --[[ @@ -38,12 +38,6 @@ export type ResponsePayload = { config: ResponseOptions, } -export type NextResponse = { - status: number?, - body: any, - headers: { [string]: string }?, -} - export type Response = { --[[ Respond with a string @@ -71,4 +65,26 @@ export type Response = { css: (body: string, config: ResponseOptions?) -> ResponsePayload, } +-- Validators +export type BodyField = { + type: string?, + required: boolean?, + min: number?, + max: number?, + pattern: string?, +} + +export type StringField = { + required: boolean?, + minLength: number?, + maxLength: number?, + pattern: string?, +} + +export type ValidatorSchema = { + body: { [string]: BodyField }?, + params: { [string]: StringField }?, + query: { [string]: StringField }?, +} + return {} diff --git a/tests/web/.luaurc b/tests/web/.luaurc index a6caadc..3c36cbe 100644 --- a/tests/web/.luaurc +++ b/tests/web/.luaurc @@ -1,10 +1,10 @@ { "aliases": { - "lune": "~/.lune/.typedefs/0.10.4/", "std": "~/.lute/typedefs/1.0.0/std", "c": "/home/wiz/projects/nova-projects/nova_core/tests/web/", "lint": "~/.lute/typedefs/1.0.0/lint", "nova": "../../src/index", + "lune": "~/.lune/.typedefs/0.10.4/", "pkg": "./lune_packages/", "lute": "~/.lute/typedefs/1.0.0/lute" } diff --git a/tests/web/src/app/route.luau b/tests/web/src/app/route.luau index 5a48982..c6bd8a8 100644 --- a/tests/web/src/app/route.luau +++ b/tests/web/src/app/route.luau @@ -8,4 +8,9 @@ function Home.Get() return Nova.response.send("Hello, World") end +--@Validator(createUser) +function Home.Post(req: Nova.Request) + return Nova.response.json({ msg = "Got the Response", data = req.body }) +end + return Home diff --git a/tests/web/src/validators/createUser.luau b/tests/web/src/validators/createUser.luau new file mode 100644 index 0000000..3972e50 --- /dev/null +++ b/tests/web/src/validators/createUser.luau @@ -0,0 +1,11 @@ +local Nova = require("../../../../src/types") + +local schema: Nova.ValidatorSchema = { + body = { + username = { type = "string", required = true, min = 3, max = 20 }, + age = { type = "number", required = true, min = 18 }, + email = { type = "string", required = true, pattern = "^[^@%s]+@[^@%s]+%.[^@%s]+$" }, + } +} + +return schema \ No newline at end of file