44//
55// GET /api/v1/messages — list messages (query: status, limit)
66// GET /api/v1/messages/{id} — get a single message by ID
7+ // GET /api/v1/channels — list channels
8+ // POST /api/v1/channels — create channel
9+ // GET /api/v1/channels/{id} — get channel by ID
10+ // PUT /api/v1/channels/{id} — update channel
11+ // DELETE /api/v1/channels/{id} — delete channel
12+ // GET /healthz — liveness probe
13+ // GET /readyz — readiness probe (checks store)
714//
815// All responses are JSON. Error bodies have the shape {"error":"reason"}.
916// The server uses Go 1.22 enhanced ServeMux patterns, so unknown methods
1017// receive an automatic 405 and missing routes receive 404.
1118package rest
1219
1320import (
21+ "context"
1422 "database/sql"
1523 "encoding/json"
1624 "errors"
1725 "net/http"
1826 "strconv"
1927 "time"
2028
29+ "github.com/vagnercazarotto/verifhir-gateway/internal/channel"
2130 "github.com/vagnercazarotto/verifhir-gateway/internal/store"
2231)
2332
@@ -43,18 +52,30 @@ type MessageResponse struct {
4352 UpdatedAt time.Time `json:"updated_at"`
4453}
4554
46- // Server is an http.Handler that exposes the message store as a REST API.
55+ // Server is an http.Handler that exposes the message store and channel
56+ // registry as a REST API.
4757type Server struct {
48- store store.Store
49- mux * http.ServeMux
58+ store store.Store
59+ channels * channel.Registry
60+ mux * http.ServeMux
5061}
5162
52- // New creates a Server backed by st and registers all routes.
53- func New (st store.Store ) * Server {
54- s := & Server {store : st }
63+ // New creates a Server backed by st and reg, and registers all routes.
64+ func New (st store.Store , reg * channel. Registry ) * Server {
65+ s := & Server {store : st , channels : reg }
5566 s .mux = http .NewServeMux ()
67+ // message routes
5668 s .mux .HandleFunc ("GET /api/v1/messages" , s .listMessages )
5769 s .mux .HandleFunc ("GET /api/v1/messages/{id}" , s .getMessage )
70+ // channel routes
71+ s .mux .HandleFunc ("GET /api/v1/channels" , s .listChannels )
72+ s .mux .HandleFunc ("POST /api/v1/channels" , s .createChannel )
73+ s .mux .HandleFunc ("GET /api/v1/channels/{id}" , s .getChannel )
74+ s .mux .HandleFunc ("PUT /api/v1/channels/{id}" , s .updateChannel )
75+ s .mux .HandleFunc ("DELETE /api/v1/channels/{id}" , s .deleteChannel )
76+ // health routes
77+ s .mux .HandleFunc ("GET /healthz" , s .healthz )
78+ s .mux .HandleFunc ("GET /readyz" , s .readyz )
5879 return s
5980}
6081
@@ -137,6 +158,94 @@ func recordToResponse(rec *store.Record) MessageResponse {
137158 }
138159}
139160
161+ // ---- channel handlers -----------------------------------------------------
162+
163+ // listChannels handles GET /api/v1/channels
164+ func (s * Server ) listChannels (w http.ResponseWriter , r * http.Request ) {
165+ writeJSON (w , http .StatusOK , s .channels .List ())
166+ }
167+
168+ // createChannel handles POST /api/v1/channels
169+ func (s * Server ) createChannel (w http.ResponseWriter , r * http.Request ) {
170+ var ch channel.Channel
171+ if err := json .NewDecoder (r .Body ).Decode (& ch ); err != nil {
172+ writeError (w , http .StatusBadRequest , "invalid JSON body" )
173+ return
174+ }
175+ if ch .ID == "" {
176+ writeError (w , http .StatusBadRequest , "id is required" )
177+ return
178+ }
179+ if ch .URL == "" {
180+ writeError (w , http .StatusBadRequest , "url is required" )
181+ return
182+ }
183+ if err := s .channels .Add (ch ); err != nil {
184+ if errors .Is (err , channel .ErrDuplicateID ) {
185+ writeError (w , http .StatusConflict , "channel ID already exists" )
186+ return
187+ }
188+ writeError (w , http .StatusInternalServerError , "failed to create channel" )
189+ return
190+ }
191+ got , _ := s .channels .Get (ch .ID )
192+ writeJSON (w , http .StatusCreated , got )
193+ }
194+
195+ // getChannel handles GET /api/v1/channels/{id}
196+ func (s * Server ) getChannel (w http.ResponseWriter , r * http.Request ) {
197+ id := r .PathValue ("id" )
198+ ch , err := s .channels .Get (id )
199+ if err != nil {
200+ if errors .Is (err , channel .ErrNotFound ) {
201+ writeError (w , http .StatusNotFound , "channel not found" )
202+ return
203+ }
204+ writeError (w , http .StatusInternalServerError , "failed to retrieve channel" )
205+ return
206+ }
207+ writeJSON (w , http .StatusOK , ch )
208+ }
209+
210+ // updateChannel handles PUT /api/v1/channels/{id}
211+ func (s * Server ) updateChannel (w http.ResponseWriter , r * http.Request ) {
212+ id := r .PathValue ("id" )
213+ var ch channel.Channel
214+ if err := json .NewDecoder (r .Body ).Decode (& ch ); err != nil {
215+ writeError (w , http .StatusBadRequest , "invalid JSON body" )
216+ return
217+ }
218+ ch .ID = id // path wins over body
219+ if ch .URL == "" {
220+ writeError (w , http .StatusBadRequest , "url is required" )
221+ return
222+ }
223+ if err := s .channels .Update (ch ); err != nil {
224+ if errors .Is (err , channel .ErrNotFound ) {
225+ writeError (w , http .StatusNotFound , "channel not found" )
226+ return
227+ }
228+ writeError (w , http .StatusInternalServerError , "failed to update channel" )
229+ return
230+ }
231+ got , _ := s .channels .Get (id )
232+ writeJSON (w , http .StatusOK , got )
233+ }
234+
235+ // deleteChannel handles DELETE /api/v1/channels/{id}
236+ func (s * Server ) deleteChannel (w http.ResponseWriter , r * http.Request ) {
237+ id := r .PathValue ("id" )
238+ if err := s .channels .Delete (id ); err != nil {
239+ if errors .Is (err , channel .ErrNotFound ) {
240+ writeError (w , http .StatusNotFound , "channel not found" )
241+ return
242+ }
243+ writeError (w , http .StatusInternalServerError , "failed to delete channel" )
244+ return
245+ }
246+ w .WriteHeader (http .StatusNoContent )
247+ }
248+
140249func writeJSON (w http.ResponseWriter , status int , v any ) {
141250 w .Header ().Set ("Content-Type" , "application/json" )
142251 w .WriteHeader (status )
@@ -146,3 +255,32 @@ func writeJSON(w http.ResponseWriter, status int, v any) {
146255func writeError (w http.ResponseWriter , status int , msg string ) {
147256 writeJSON (w , status , map [string ]string {"error" : msg })
148257}
258+
259+ // pinger is an optional interface that store backends may implement.
260+ // If the backing store satisfies pinger, readyz will call Ping to verify
261+ // database connectivity.
262+ type pinger interface {
263+ Ping (ctx context.Context ) error
264+ }
265+
266+ // healthz handles GET /healthz — liveness probe.
267+ // Always returns 200 {"status":"ok"} while the process is running.
268+ func (s * Server ) healthz (w http.ResponseWriter , _ * http.Request ) {
269+ writeJSON (w , http .StatusOK , map [string ]string {"status" : "ok" })
270+ }
271+
272+ // readyz handles GET /readyz — readiness probe.
273+ // Returns 200 {"status":"ok"} when the store is reachable,
274+ // or 503 {"status":"unavailable","reason":"..."} otherwise.
275+ func (s * Server ) readyz (w http.ResponseWriter , r * http.Request ) {
276+ if p , ok := s .store .(pinger ); ok {
277+ if err := p .Ping (r .Context ()); err != nil {
278+ writeJSON (w , http .StatusServiceUnavailable , map [string ]any {
279+ "status" : "unavailable" ,
280+ "reason" : err .Error (),
281+ })
282+ return
283+ }
284+ }
285+ writeJSON (w , http .StatusOK , map [string ]string {"status" : "ok" })
286+ }
0 commit comments