feat(api): accept rule config block string in playground

Update playground request to take rules as a string and parse
either YAML list or DSL config, with tests and swagger updates.
This commit is contained in:
yusing
2026-02-24 18:11:17 +08:00
parent 0eba045104
commit ed2ca236b0
4 changed files with 110 additions and 75 deletions

View File

@@ -5125,10 +5125,7 @@
"$ref": "#/definitions/MockResponse"
},
"rules": {
"type": "array",
"items": {
"$ref": "#/definitions/routeApi.RawRule"
},
"type": "string",
"x-nullable": false,
"x-omitempty": false
}
@@ -6926,28 +6923,6 @@
"x-nullable": false,
"x-omitempty": false
},
"routeApi.RawRule": {
"type": "object",
"properties": {
"do": {
"type": "string",
"x-nullable": false,
"x-omitempty": false
},
"name": {
"type": "string",
"x-nullable": false,
"x-omitempty": false
},
"on": {
"type": "string",
"x-nullable": false,
"x-omitempty": false
}
},
"x-nullable": false,
"x-omitempty": false
},
"routeApi.RoutesByProvider": {
"type": "object",
"additionalProperties": {

View File

@@ -905,9 +905,7 @@ definitions:
mockResponse:
$ref: '#/definitions/MockResponse'
rules:
items:
$ref: '#/definitions/routeApi.RawRule'
type: array
type: string
required:
- rules
type: object
@@ -1858,15 +1856,6 @@ definitions:
uptime:
type: string
type: object
routeApi.RawRule:
properties:
do:
type: string
name:
type: string
"on":
type: string
type: object
routeApi.RoutesByProvider:
additionalProperties:
items:

View File

@@ -9,6 +9,7 @@ import (
"strings"
"github.com/gin-gonic/gin"
"github.com/goccy/go-yaml"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/route/rules"
apitypes "github.com/yusing/goutils/apitypes"
@@ -23,7 +24,7 @@ type RawRule struct {
}
type PlaygroundRequest struct {
Rules []RawRule `json:"rules" binding:"required"`
Rules string `json:"rules" binding:"required"`
MockRequest MockRequest `json:"mockRequest"`
MockResponse MockResponse `json:"mockResponse"`
} // @name PlaygroundRequest
@@ -255,7 +256,35 @@ func handlerWithRecover(w http.ResponseWriter, r *http.Request, h http.HandlerFu
h(w, r)
}
func parseRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, error) {
func parseRules(config string) ([]ParsedRule, rules.Rules, error) {
config = strings.TrimSpace(config)
if config == "" {
return []ParsedRule{}, nil, nil
}
var rawRules []RawRule
if err := yaml.Unmarshal([]byte(config), &rawRules); err == nil && len(rawRules) > 0 {
return parseRawRules(rawRules)
}
var rulesList rules.Rules
if err := rulesList.Parse(config); err != nil {
return nil, nil, err
}
parsedRules := make([]ParsedRule, 0, len(rulesList))
for _, rule := range rulesList {
parsedRules = append(parsedRules, ParsedRule{
Name: rule.Name,
On: rule.On.String(),
Do: rule.Do.String(),
})
}
return parsedRules, rulesList, nil
}
func parseRawRules(rawRules []RawRule) ([]ParsedRule, rules.Rules, error) {
parsedRules := make([]ParsedRule, 0, len(rawRules))
rulesList := make(rules.Rules, 0, len(rawRules))

View File

@@ -22,13 +22,10 @@ func TestPlayground(t *testing.T) {
{
name: "simple path matching rule",
request: PlaygroundRequest{
Rules: []RawRule{
{
Name: "test rule",
On: "path /api",
Do: "pass",
},
},
Rules: `- name: test rule
on: path /api
do: pass
`,
MockRequest: MockRequest{
Method: "GET",
Path: "/api",
@@ -53,13 +50,10 @@ func TestPlayground(t *testing.T) {
{
name: "header matching rule",
request: PlaygroundRequest{
Rules: []RawRule{
{
Name: "check user agent",
On: "header User-Agent Chrome",
Do: "error 403 Forbidden",
},
},
Rules: `- name: check user agent
on: header User-Agent Chrome
do: error 403 Forbidden
`,
MockRequest: MockRequest{
Method: "GET",
Path: "/",
@@ -90,13 +84,10 @@ func TestPlayground(t *testing.T) {
{
name: "invalid rule syntax",
request: PlaygroundRequest{
Rules: []RawRule{
{
Name: "bad rule",
On: "invalid_checker something",
Do: "pass",
},
},
Rules: `- name: bad rule
on: invalid_checker something
do: pass
`,
MockRequest: MockRequest{
Method: "GET",
Path: "/",
@@ -115,13 +106,10 @@ func TestPlayground(t *testing.T) {
{
name: "rewrite path rule",
request: PlaygroundRequest{
Rules: []RawRule{
{
Name: "rewrite rule",
On: "path glob(/api/*)",
Do: "rewrite /api/ /v1/",
},
},
Rules: `- name: rewrite rule
on: path glob(/api/*)
do: rewrite /api/ /v1/
`,
MockRequest: MockRequest{
Method: "GET",
Path: "/api/users",
@@ -148,13 +136,10 @@ func TestPlayground(t *testing.T) {
{
name: "method matching rule",
request: PlaygroundRequest{
Rules: []RawRule{
{
Name: "block POST",
On: "method POST",
Do: `error "405" "Method Not Allowed"`,
},
},
Rules: `- name: block POST
on: method POST
do: error "405" "Method Not Allowed"
`,
MockRequest: MockRequest{
Method: "POST",
Path: "/api",
@@ -173,6 +158,63 @@ func TestPlayground(t *testing.T) {
}
},
},
{
name: "block syntax default rule",
request: PlaygroundRequest{
Rules: `default {
pass
}`,
MockRequest: MockRequest{
Method: "GET",
Path: "/",
},
},
wantStatusCode: http.StatusOK,
checkResponse: func(t *testing.T, resp PlaygroundResponse) {
if len(resp.ParsedRules) != 1 {
t.Errorf("expected 1 parsed rule, got %d", len(resp.ParsedRules))
}
if resp.ParsedRules[0].ValidationError != nil {
t.Errorf("expected rule to be valid, got error: %v", resp.ParsedRules[0].ValidationError)
}
if !resp.UpstreamCalled {
t.Error("expected upstream to be called")
}
},
},
{
name: "block syntax conditional rule",
request: PlaygroundRequest{
Rules: `header User-Agent Chrome {
error 403 Forbidden
}`,
MockRequest: MockRequest{
Method: "GET",
Path: "/",
Headers: map[string][]string{
"User-Agent": {"Chrome"},
},
},
},
wantStatusCode: http.StatusOK,
checkResponse: func(t *testing.T, resp PlaygroundResponse) {
if len(resp.ParsedRules) != 1 {
t.Errorf("expected 1 parsed rule, got %d", len(resp.ParsedRules))
}
if resp.ParsedRules[0].ValidationError != nil {
t.Errorf("expected rule to be valid, got error: %v", resp.ParsedRules[0].ValidationError)
}
if len(resp.MatchedRules) != 1 {
t.Errorf("expected 1 matched rule, got %d", len(resp.MatchedRules))
}
if resp.FinalResponse.StatusCode != http.StatusForbidden {
t.Errorf("expected status 403, got %d", resp.FinalResponse.StatusCode)
}
if resp.UpstreamCalled {
t.Error("expected upstream not to be called")
}
},
},
}
for _, tt := range tests {