Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,8 @@ jobs:
run: |
export PATH=$OPENRESTY_PREFIX/nginx/sbin:$OPENRESTY_PREFIX/bin:$PATH
make test

- name: Lint
run: |
sudo luarocks install luacheck
make lint
5 changes: 5 additions & 0 deletions .luacheckrc
Comment thread
jarvis9443 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
std = "ngx_lua"
ignore = {
"542", -- empty if branch
}
redefined = false
31 changes: 28 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,26 +1,51 @@
.PHONY: test test-unit test-conformance benchmark lint clean
INST_PREFIX ?= /usr/local/openresty
INST_LUADIR ?= $(INST_PREFIX)/lualib
INSTALL ?= install

RESTY := /usr/local/openresty/bin/resty --shdict "test 1m"

UNIT_TESTS := $(sort $(wildcard t/unit/test_*.lua))
CONFORMANCE_TESTS := $(sort $(wildcard t/conformance/test_*.lua))

.PHONY: test test-unit test-conformance benchmark lint dev install clean help

### help: Show Makefile rules
help:
@echo Makefile rules:
@echo
@grep -E '^### [-A-Za-z0-9_]+:' Makefile | sed 's/###/ /'

### dev: Create a development ENV
dev:
luarocks install rockspec/lua-resty-openapi-validator-master-0.1-0.rockspec --only-deps --local

### install: Install the library to runtime
install:
$(INSTALL) -d $(INST_LUADIR)/resty/openapi_validator/
$(INSTALL) lib/resty/openapi_validator/*.lua $(INST_LUADIR)/resty/openapi_validator/

### test: Run all tests
test: test-unit test-conformance
@echo "All tests passed."

### test-unit: Run unit tests
test-unit:
@echo "=== Unit tests ==="
@for f in $(UNIT_TESTS); do $(RESTY) -e "dofile('$$f')" || exit 1; done

### test-conformance: Run conformance tests
test-conformance:
@echo "=== Conformance tests ==="
@for f in $(CONFORMANCE_TESTS); do $(RESTY) -e "dofile('$$f')" || exit 1; done

### benchmark: Run microbenchmark
benchmark:
@$(RESTY) -e 'dofile("benchmark/bench.lua")'

### lint: Lint Lua source code
lint:
@luacheck lib/ --std ngx_lua
luacheck -q lib/

### clean: Remove build artifacts
clean:
@rm -rf *.rock benchmark/logs/ benchmark/nginx.conf
rm -rf *.rock benchmark/logs/ benchmark/nginx.conf
102 changes: 70 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,66 @@
# lua-resty-openapi-validator
Name
====

Pure Lua OpenAPI request validator for OpenResty / LuaJIT.
lua-resty-openapi-validator - Pure Lua OpenAPI request validator for OpenResty / LuaJIT.

![CI](https://github.com/api7/lua-resty-openapi-validator/actions/workflows/test.yml/badge.svg)
![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)

Table of Contents
=================

* [Description](#description)
* [Install](#install)
* [Quick Start](#quick-start)
* [API](api.md)
* [Validation Scope](#validation-scope)
* [OpenAPI 3.1 Support](#openapi-31-support)
* [Benchmark](#benchmark)
* [Testing](#testing)

Description
===========

Validates HTTP requests against OpenAPI 3.0 and 3.1 specifications using
[lua-resty-radixtree](https://github.com/api7/lua-resty-radixtree) for path
matching and [api7/jsonschema](https://github.com/api7/jsonschema) for schema
validation. No Go FFI or external processes required.

## Performance
Install
=======

**~45% higher throughput** than the Go FFI-based validator under concurrent load
(single worker, 50 connections). See [benchmark/RESULTS.md](benchmark/RESULTS.md).
> Dependencies

## Installation
- [api7/jsonschema](https://github.com/api7/jsonschema) — JSON Schema Draft 4/6/7 validation
- [lua-resty-radixtree](https://github.com/api7/lua-resty-radixtree) — radix tree path routing
- [lua-cjson](https://github.com/openresty/lua-cjson) — JSON encoding/decoding

> install by luarocks

```bash
```shell
luarocks install lua-resty-openapi-validator
```

Or add the `lib/` directory to your `lua_package_path`.
> install by source

### Dependencies
```shell
git clone https://github.com/api7/lua-resty-openapi-validator.git
cd lua-resty-openapi-validator
make dev
sudo make install
```

- [api7/jsonschema](https://github.com/api7/jsonschema) — JSON Schema Draft 4/6/7 validation
- [lua-resty-radixtree](https://github.com/api7/lua-resty-radixtree) — radix tree path routing
- [lua-cjson](https://github.com/openresty/lua-cjson) — JSON encoding/decoding
[Back to TOC](#table-of-contents)

## Quick Start
Quick Start
===========

```lua
local ov = require("resty.openapi_validator")

-- compile once (cache the result)
local validator, err = ov.compile(spec_json_string, {
strict = true, -- error on unsupported 3.1 keywords (default: true)
coerce_types = true, -- coerce query/header string values to schema types (default: true)
fail_fast = false, -- return on first error (default: false)
strict = true, -- error on unsupported 3.1 keywords (default: true)
})
if not validator then
ngx.log(ngx.ERR, "spec compile error: ", err)
Expand All @@ -59,38 +84,37 @@ if not ok then
end
```

### Selective Validation
See [API documentation](api.md) for details on all methods and options.

Skip specific validation steps:
[Back to TOC](#table-of-contents)

```lua
local ok, err = validator:validate_request(req, {
skip_query = true, -- skip query parameter validation
skip_body = true, -- skip request body validation
})
```

## Validation Scope
Validation Scope
================

| Feature | Status |
|---|---|
| Path parameter matching & validation | ✅ |
| Query parameter validation (with type coercion) | ✅ |
| Header validation | ✅ |
| Request body validation (JSON) | ✅ |
| Request body validation (form-urlencoded) | ✅ |
| `style` / `explode` parameter serialization | ✅ |
| `$ref` resolution (document-internal) | ✅ |
| Circular `$ref` support | ✅ |
| `allOf` / `oneOf` / `anyOf` composition | ✅ |
| `additionalProperties` | ✅ |
| OpenAPI 3.0 `nullable` | ✅ |
| OpenAPI 3.1 type arrays (`["string", "null"]`) | ✅ |
| `readOnly` / `writeOnly` validation | ✅ |
| Response validation | ❌ (not planned for v1) |
| Security scheme validation | ❌ |
| External `$ref` (URLs, files) | ❌ |
| `multipart/form-data` body | ⚠️ (skipped, returns OK) |
| `multipart/form-data` body | ⚠️ basic support |

[Back to TOC](#table-of-contents)

## OpenAPI 3.1 Support
OpenAPI 3.1 Support
===================

OpenAPI 3.1 uses JSON Schema Draft 2020-12. Since the underlying jsonschema
library supports up to Draft 7, schemas are normalized at compile time:
Expand All @@ -104,15 +128,29 @@ library supports up to Draft 7, schemas are normalized at compile time:
| `$ref` with sibling keywords | → `allOf: [resolved, {siblings}]` |
| `$dynamicRef`, `unevaluatedProperties` | Error (strict) / Warning (lenient) |

## Testing
[Back to TOC](#table-of-contents)

Benchmark
=========

```bash
**~45% higher throughput** than the Go FFI-based validator under concurrent load
(single worker, 50 connections). See [benchmark/RESULTS.md](benchmark/RESULTS.md).

[Back to TOC](#table-of-contents)

Testing
=======

```shell
make test
```

Runs 200 tests across unit tests and conformance tests ported from
Runs unit tests and conformance tests ported from
[kin-openapi](https://github.com/getkin/kin-openapi).

## License
[Back to TOC](#table-of-contents)

License
=======

Apache 2.0
91 changes: 91 additions & 0 deletions api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
API
===

Table of Contents
=================

* [compile](#compile)
* [validate_request](#validate_request)

compile
-------

`syntax: validator, err = ov.compile(spec_str, opts)`

Compiles an OpenAPI specification JSON string into a reusable validator object.
The spec is parsed, `$ref` pointers are resolved, and schemas are normalized to
JSON Schema Draft 7. The returned validator should be cached and reused across
requests.

- `spec_str`: string — raw JSON of an OpenAPI 3.0 or 3.1 specification.
- `opts`: table (optional) — compilation options:
- `strict`: boolean (default `true`) — if `true`, returns an error when
unsupported OpenAPI 3.1 keywords are encountered (`$dynamicRef`,
`unevaluatedProperties`, etc.); if `false`, these keywords are silently
dropped with a warning.

Returns a validator object on success, or `nil` and an error string on failure.

```lua
local ov = require("resty.openapi_validator")

local validator, err = ov.compile(spec_json, { strict = true })
if not validator then
ngx.log(ngx.ERR, "compile: ", err)
return
end
```

[Back to TOC](#table-of-contents)

validate_request
----------------

`syntax: ok, err = validator:validate_request(req, skip)`

Validates an incoming HTTP request against the compiled OpenAPI spec. Returns
`true` on success, or `false` and a formatted error string on failure.

- `req`: table — request data with the following fields:
- `method`: string (required) — HTTP method (e.g. `"GET"`, `"POST"`)
- `path`: string (required) — request URI path (e.g. `"/users/123"`)
- `query`: table (optional) — query parameters `{ name = value | {values} }`
- `headers`: table (optional) — request headers `{ name = value }`
- `body`: string (optional) — raw request body
- `content_type`: string (optional) — Content-Type header value

- `skip`: table (optional) — selectively skip validation steps:
- `path`: boolean — skip path parameter validation
- `query`: boolean — skip query parameter validation
- `header`: boolean — skip header validation
- `body`: boolean — skip request body validation
- `read_only`: boolean — skip readOnly property checks in request body
- `write_only`: boolean — skip writeOnly property checks

```lua
local ok, err = validator:validate_request({
method = ngx.req.get_method(),
path = ngx.var.uri,
query = ngx.req.get_uri_args(),
headers = ngx.req.get_headers(0, true),
body = ngx.req.get_body_data(),
content_type = ngx.var.content_type,
})

if not ok then
ngx.status = 400
ngx.say(err)
return
end
```

Skip specific validation:

```lua
local ok, err = validator:validate_request(req, {
body = true, -- skip body validation
read_only = true, -- skip readOnly checks
})
```

[Back to TOC](#table-of-contents)
Loading
Loading