Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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