5096042b
feat: add Match and SaveResponse to middleware
a73x 2026-03-31 05:47
Commit message
middleware/middleware.go
| Old | New | ||
|---|---|---|---|
| @@ -57,3 +57,21 @@ func New(path string) (*Middleware, error) { | |||
| 57 | func (m *Middleware) RuleCount() int { | 57 | func (m *Middleware) RuleCount() int { |
| 58 | return len(m.rules) | 58 | return len(m.rules) |
| 59 | } | 59 | } |
| 60 | |||
| 61 | // Match checks if host+path matches any middleware rule. | ||
| 62 | // Returns the first matching rule, or nil. | ||
| 63 | func (m *Middleware) Match(host, path string) *Rule { | ||
| 64 | url := host + path | ||
| 65 | for i := range m.rules { | ||
| 66 | if m.rules[i].Match == url { | ||
| 67 | return &m.rules[i] | ||
| 68 | } | ||
| 69 | } | ||
| 70 | return nil | ||
| 71 | } | ||
| 72 | |||
| 73 | // SaveResponse writes the response body to the rule's destination file, | ||
| 74 | // overwriting any existing content. | ||
| 75 | func (r *Rule) SaveResponse(body []byte) error { | ||
| 76 | return os.WriteFile(r.Dest, body, 0644) | ||
| 77 | } | ||
middleware/middleware_test.go
| Old | New | ||
|---|---|---|---|
| @@ -50,3 +50,80 @@ func TestNewRejectsUnknownAction(t *testing.T) { | |||
| 50 | t.Fatal("expected error for unknown action") | 50 | t.Fatal("expected error for unknown action") |
| 51 | } | 51 | } |
| 52 | } | 52 | } |
| 53 | |||
| 54 | func TestMatchReturnsRuleForMatchingURL(t *testing.T) { | ||
| 55 | dir := t.TempDir() | ||
| 56 | path := filepath.Join(dir, "middleware.yaml") | ||
| 57 | os.WriteFile(path, []byte(`middleware: | ||
| 58 | - match: "api.anthropic.com/api/oauth/usage" | ||
| 59 | action: save_response | ||
| 60 | dest: "/tmp/usage.json" | ||
| 61 | `), 0644) | ||
| 62 | |||
| 63 | mw, _ := middleware.New(path) | ||
| 64 | rule := mw.Match("api.anthropic.com", "/api/oauth/usage") | ||
| 65 | if rule == nil { | ||
| 66 | t.Fatal("expected a match") | ||
| 67 | } | ||
| 68 | if rule.Dest != "/tmp/usage.json" { | ||
| 69 | t.Errorf("expected dest /tmp/usage.json, got %s", rule.Dest) | ||
| 70 | } | ||
| 71 | } | ||
| 72 | |||
| 73 | func TestMatchReturnsNilForNoMatch(t *testing.T) { | ||
| 74 | dir := t.TempDir() | ||
| 75 | path := filepath.Join(dir, "middleware.yaml") | ||
| 76 | os.WriteFile(path, []byte(`middleware: | ||
| 77 | - match: "api.anthropic.com/api/oauth/usage" | ||
| 78 | action: save_response | ||
| 79 | dest: "/tmp/usage.json" | ||
| 80 | `), 0644) | ||
| 81 | |||
| 82 | mw, _ := middleware.New(path) | ||
| 83 | rule := mw.Match("example.com", "/other") | ||
| 84 | if rule != nil { | ||
| 85 | t.Fatal("expected no match") | ||
| 86 | } | ||
| 87 | } | ||
| 88 | |||
| 89 | func TestSaveResponseWritesBodyToFile(t *testing.T) { | ||
| 90 | dest := filepath.Join(t.TempDir(), "out.json") | ||
| 91 | rule := &middleware.Rule{ | ||
| 92 | Match: "example.com/data", | ||
| 93 | Action: "save_response", | ||
| 94 | Dest: dest, | ||
| 95 | } | ||
| 96 | |||
| 97 | body := []byte(`{"tokens": 42}`) | ||
| 98 | err := rule.SaveResponse(body) | ||
| 99 | if err != nil { | ||
| 100 | t.Fatalf("unexpected error: %v", err) | ||
| 101 | } | ||
| 102 | |||
| 103 | got, err := os.ReadFile(dest) | ||
| 104 | if err != nil { | ||
| 105 | t.Fatalf("failed to read dest file: %v", err) | ||
| 106 | } | ||
| 107 | if string(got) != string(body) { | ||
| 108 | t.Errorf("expected %q, got %q", body, got) | ||
| 109 | } | ||
| 110 | } | ||
| 111 | |||
| 112 | func TestSaveResponseOverwritesExistingFile(t *testing.T) { | ||
| 113 | dest := filepath.Join(t.TempDir(), "out.json") | ||
| 114 | os.WriteFile(dest, []byte("old data"), 0644) | ||
| 115 | |||
| 116 | rule := &middleware.Rule{ | ||
| 117 | Match: "example.com/data", | ||
| 118 | Action: "save_response", | ||
| 119 | Dest: dest, | ||
| 120 | } | ||
| 121 | |||
| 122 | body := []byte(`{"new": true}`) | ||
| 123 | rule.SaveResponse(body) | ||
| 124 | |||
| 125 | got, _ := os.ReadFile(dest) | ||
| 126 | if string(got) != string(body) { | ||
| 127 | t.Errorf("expected %q, got %q", body, got) | ||
| 128 | } | ||
| 129 | } | ||