Skip to content

Commit 8407831

Browse files
committed
feat: Enhance OpenAPI provider to build paths from application routes
- Implemented `build_paths` method in OpenAPIProvider to convert application routes into OpenAPI path items. - Added support for multiple route representations including `RouteDescriptor` and direct OpenAPI paths. - Updated OpenAPI schema generation to include dynamically built paths. - Introduced new WebhookEventType `GatewayShutdown` for gateway shutdown notifications. - Refactored routing strategy to use `Service` as the default mount strategy. - Added comprehensive tests for federated schema handling and OpenAPI generation. - Introduced `FederatedSchemaHandler` to serve merged schema endpoints with caching. - Enhanced error handling and response validation in the federated handler.
1 parent 09ce459 commit 8407831

17 files changed

Lines changed: 2112 additions & 57 deletions

File tree

discovery/node.go

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package discovery
22

33
import (
4+
"bytes"
45
"context"
56
"crypto/rand"
67
"encoding/hex"
8+
"encoding/json"
79
"fmt"
810
"net/http"
911
"strings"
@@ -233,7 +235,7 @@ type serviceApp struct {
233235

234236
func (a *serviceApp) Name() string { return a.config.ServiceName }
235237
func (a *serviceApp) Version() string { return a.config.ServiceVersion }
236-
func (a *serviceApp) Routes() any { return nil }
238+
func (a *serviceApp) Routes() any { return a.config.Routes }
237239

238240
func (n *ServiceNode) generateSchemas(ctx context.Context) error {
239241
app := &serviceApp{config: &n.config}
@@ -501,7 +503,12 @@ func (n *GatewayNode) Start(ctx context.Context) error {
501503
}
502504

503505
// Stop stops watching and cleans up.
506+
// Before shutting down, it sends a fire-and-forget gateway.shutdown notification
507+
// to all services that have a webhook endpoint configured.
504508
func (n *GatewayNode) Stop(_ context.Context) error {
509+
// Notify services before stopping (fire-and-forget)
510+
n.notifyServicesShutdown()
511+
505512
if n.cancel != nil {
506513
n.cancel()
507514
<-n.done
@@ -660,6 +667,67 @@ func (n *GatewayNode) notifyRouteChange() {
660667
}
661668
}
662669

670+
// notifyServicesShutdown sends a gateway.shutdown event to all services that
671+
// have a webhook endpoint configured. This is fire-and-forget: each notification
672+
// is dispatched in its own goroutine with a short timeout, and errors are silently
673+
// discarded. The gateway does not wait for responses.
674+
func (n *GatewayNode) notifyServicesShutdown() {
675+
n.mu.RLock()
676+
manifests := make([]*farp.SchemaManifest, 0, len(n.manifests))
677+
for _, m := range n.manifests {
678+
manifests = append(manifests, m)
679+
}
680+
n.mu.RUnlock()
681+
682+
if len(manifests) == 0 {
683+
return
684+
}
685+
686+
event := farp.WebhookEvent{
687+
Type: farp.EventGatewayShutdown,
688+
Timestamp: time.Now().Unix(),
689+
Source: "gateway",
690+
}
691+
692+
payload, err := json.Marshal(event)
693+
if err != nil {
694+
return
695+
}
696+
697+
client := n.config.HTTPClient
698+
if client == nil {
699+
client = &http.Client{Timeout: 3 * time.Second}
700+
}
701+
702+
for _, manifest := range manifests {
703+
webhookURL := manifest.Webhook.ServiceWebhook
704+
if webhookURL == "" {
705+
continue
706+
}
707+
708+
// Fire and forget — each in its own goroutine
709+
go func(url string) {
710+
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
711+
defer cancel()
712+
713+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload))
714+
if err != nil {
715+
return
716+
}
717+
718+
req.Header.Set("Content-Type", "application/json")
719+
req.Header.Set("X-Farp-Event", string(farp.EventGatewayShutdown))
720+
721+
resp, err := client.Do(req)
722+
if err != nil {
723+
return
724+
}
725+
726+
resp.Body.Close()
727+
}(webhookURL)
728+
}
729+
}
730+
663731
// =============================================================================
664732
// Helpers
665733
// =============================================================================

discovery/options.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ type ServiceNodeConfig struct {
5151
MountStrategy farp.MountStrategy
5252
BasePath string
5353

54+
// Routes provides route information for OpenAPI schema generation.
55+
// Can be []farp.RouteDescriptor, map[string]any (OpenAPI paths), or any
56+
// type that the configured schema provider understands.
57+
// If nil, the provider's Generate() may fail or produce empty paths.
58+
Routes any
59+
5460
// Service hints (flows into manifest)
5561
Hints *farp.ServiceHints
5662

docs/ARCHITECTURE.md

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,18 @@
66

77
FARP is a **protocol specification library**, NOT a complete gateway or service framework. This document clarifies what FARP provides versus what implementers (services and gateways) must build.
88

9-
### Responsibility Matrix
9+
### Responsibility Matrix (v1.1.0)
1010

1111
| Concern | FARP Library | Service Implementation | Gateway Implementation |
1212
|---------|--------------|------------------------|------------------------|
1313
| **Data Structures** | ✅ Defines types | Uses types | Uses types |
1414
| **Schema Generation** | ✅ Providers | Calls providers | - |
1515
| **Schema Merging** | ✅ Merge logic | - | Calls merge logic |
16-
| **HTTP Endpoints** | ❌ Examples only |**Must implement** |**Must implement** |
17-
| **Service Discovery** | ❌ Interface only |**Must integrate** |**Must integrate** |
18-
| **Registry Backend** | ❌ Interface only |**Must choose/config** | **Must choose/config** |
19-
| **Route Configuration** | ❌ Examples only | - | **Must implement** |
20-
| **Health Monitoring** | ❌ Not provided |**Must expose** | **Must poll** |
16+
| **HTTP Endpoints** | `FARPHandler` | Mounts handler on router | - |
17+
| **Service Discovery** | ✅ 6 backends + push | Uses `ServiceNode` | Uses `GatewayNode` |
18+
| **Registry Backend** | ✅ Memory + KV via backends | Auto via `ServiceNode` | Auto via `GatewayNode` |
19+
| **Route Configuration** | ✅ Schema-to-route conversion | - | Applies routes from callback |
20+
| **Health Monitoring** | ✅ Auto health loop | Auto via `ServiceNode` | Auto via `GatewayNode` |
2121
| **Webhook Transport** | ❌ Types only |**Must implement** |**Must implement** |
2222

2323
### What FARP Provides (✅)
@@ -42,20 +42,45 @@ FARP is a **protocol specification library**, NOT a complete gateway or service
4242
- Checksum calculation and verification
4343
- Version compatibility checks
4444

45-
5. **Storage Abstractions** (`registry.go`)
46-
- Interface definitions only
47-
- No backend implementations (except `memory` for testing)
45+
5. **Storage Abstractions** (`registry.go`, `storage.go`)
46+
- `SchemaRegistry` and `StorageBackend` interfaces
47+
- In-memory implementation for testing (`registry/memory`)
48+
- KV-based backends via discovery backends (Consul, etcd, Redis)
49+
50+
6. **Service Discovery** (`discovery/*`)
51+
- `ServiceDiscovery` interface with 6 backend implementations
52+
- `ServiceNode` — auto-lifecycle for services (register, health, schemas)
53+
- `GatewayNode` — auto-lifecycle for gateways (discover, fetch, routes)
54+
- `FARPHandler` — ready-to-mount HTTP handler for FARP endpoints
55+
- Push-based discovery — no external registry needed
56+
57+
7. **Gateway Client** (`gateway/*`)
58+
- Schema-to-route conversion (OpenAPI, AsyncAPI, GraphQL)
59+
- Route hash comparison to prevent unnecessary remounts
60+
- Atomic route swap via `RouteUpdateHandler` interface
4861

4962
### What Service Frameworks Must Implement (Services using FARP)
5063

51-
1. **HTTP Server Endpoints**
64+
With the discovery system, most integration is automatic via `ServiceNode`:
65+
66+
```go
67+
node, _ := discovery.NewServiceNode(discovery.ServiceNodeConfig{
68+
ServiceName: "user-service",
69+
Address: "10.0.0.5:8080",
70+
Discovery: consulBackend,
71+
})
72+
node.Start(ctx)
73+
http.Handle("/_farp/", node.HTTPHandler()) // mount on your router
74+
```
75+
76+
For manual integration (without `ServiceNode`):
77+
78+
1. **HTTP Server Endpoints** — or use `FARPHandler`
5279
- `GET /_farp/manifest` - Return `SchemaManifest` JSON
53-
- `GET /openapi.json` - Return OpenAPI schema (if using HTTP location)
54-
- `GET /asyncapi.json` - Return AsyncAPI schema (if using HTTP location)
55-
- `GET /health` - Health check endpoint
56-
- `GET /metrics` - Metrics endpoint
80+
- `GET /_farp/health` - Health check endpoint
81+
- `GET /_farp/schemas/{type}` - Return schema by type
5782

58-
2. **Discovery Backend Integration**
83+
2. **Discovery Backend Integration** — or use `ServiceNode`
5984
- Register service with Consul/etcd/K8s/mDNS
6085
- Store FARP manifest in backend metadata
6186
- Handle TTL/heartbeats

0 commit comments

Comments
 (0)