Skip to content

Commit a57498d

Browse files
committed
Enhance documentation across storage and service layers
Added detailed comments for functions, methods, and structs in storage implementations (CouchDB, MongoDB, In-Memory) and gRPC server. Improved clarity on parameter usage, return values, and overall design choices to facilitate better understanding and maintainability.
1 parent 8994014 commit a57498d

File tree

8 files changed

+598
-155
lines changed

8 files changed

+598
-155
lines changed

app.go

Lines changed: 118 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Package main contains the core application logic for the Notes API.
12
package main
23

34
import (
@@ -18,129 +19,180 @@ import (
1819
"github.com/go-chi/chi/v5/middleware"
1920
)
2021

21-
// App represents the main application
22+
// App represents the main application that coordinates all components:
23+
// - Storage backend (in-memory, CouchDB, or MongoDB)
24+
// - REST API server
25+
// - gRPC API server
26+
// It handles initialization, running, and graceful shutdown of these components.
2227
type App struct {
23-
storage storage.NoteStorage
24-
restServer *http.Server
25-
grpcServer *grpc.Server
26-
config *Config
28+
storage storage.NoteStorage // Interface for storing and retrieving notes
29+
restServer *http.Server // HTTP server for REST API
30+
grpcServer *grpc.Server // gRPC server for gRPC API
31+
config *Config // Application configuration
2732
}
2833

29-
// NewApp creates a new App instance
34+
// NewApp creates a new App instance with the provided configuration.
35+
// It only initializes the App struct with the configuration; actual component
36+
// initialization happens in the Initialize method.
3037
func NewApp(config *Config) *App {
3138
return &App{
3239
config: config,
3340
}
3441
}
3542

36-
// Initialize sets up the application components
43+
// Initialize sets up the application components in the following order:
44+
// 1. Initializes the appropriate storage backend based on configuration
45+
// 2. Sets up the REST server with routes
46+
// 3. Sets up the gRPC server
47+
// This method must be called before Run.
3748
func (a *App) Initialize(ctx context.Context) error {
38-
// Initialize storage
49+
// Initialize storage backend (in-memory, CouchDB, or MongoDB)
50+
// based on the configuration
3951
storage, err := a.initializeStorage(ctx)
4052
if err != nil {
4153
return fmt.Errorf("failed to initialize storage: %w", err)
4254
}
4355
a.storage = storage
4456

45-
// Setup servers
57+
// Setup the REST and gRPC servers with the initialized storage
4658
a.restServer = a.setupRESTServer()
4759
a.grpcServer = a.setupGRPCServer()
4860

4961
return nil
5062
}
5163

52-
// Run starts the application servers
64+
// Run starts the application servers and performs the following steps:
65+
// 1. Starts the REST and gRPC servers in separate goroutines
66+
// 2. Creates sample notes in the storage
67+
// 3. Waits for a shutdown signal (e.g., Ctrl+C)
68+
// This method blocks until the application is shut down.
5369
func (a *App) Run(ctx context.Context) error {
54-
// Start servers
70+
// Start the REST and gRPC servers in separate goroutines
5571
if err := a.startServers(ctx); err != nil {
5672
return fmt.Errorf("failed to start servers: %w", err)
5773
}
5874

59-
// Create sample notes
75+
// Create sample notes in the storage for demonstration purposes
6076
if err := a.createSampleNotes(ctx); err != nil {
6177
return fmt.Errorf("failed to create sample notes: %w", err)
6278
}
6379

64-
// Wait for shutdown signal
80+
// Wait for shutdown signal (context cancellation)
81+
// This blocks until the context is canceled (e.g., by Ctrl+C)
6582
return a.waitForShutdown(ctx)
6683
}
6784

68-
// initializeStorage initializes the storage based on configuration
85+
// initializeStorage initializes the storage backend based on the configuration.
86+
// It supports three types of storage:
87+
// - "couchdb": Uses CouchDB as the storage backend
88+
// - "mongodb": Uses MongoDB as the storage backend
89+
// - Any other value (default): Uses in-memory storage
90+
//
91+
// If connecting to CouchDB or MongoDB fails, it falls back to in-memory storage
92+
// to ensure the application can still run.
6993
func (a *App) initializeStorage(ctx context.Context) (storage.NoteStorage, error) {
7094
var noteStorage storage.NoteStorage
7195
var err error
7296

97+
// Choose the storage backend based on the configuration
7398
switch a.config.StorageType {
7499
case "couchdb":
100+
// Try to connect to CouchDB
75101
log.Printf("Connecting to CouchDB at %s, database: %s", a.config.CouchDBURL, a.config.CouchDBName)
76102
noteStorage, err = storage.NewCouchDBStorage(a.config.CouchDBURL, a.config.CouchDBName)
77103
if err != nil {
104+
// If connection fails, log the error and fall back to in-memory storage
78105
log.Printf("Failed to connect to CouchDB: %v, falling back to in-memory storage", err)
79106
noteStorage = storage.NewInMemoryStorage()
80107
} else {
81108
log.Println("Successfully connected to CouchDB")
82109
}
83110
case "mongodb":
111+
// Try to connect to MongoDB
84112
log.Printf("Connecting to MongoDB at %s, database: %s, collection: %s",
85113
a.config.MongoDBURI, a.config.MongoDBName, a.config.MongoDBCollection)
86114
noteStorage, err = storage.NewMongoDBStorage(a.config.MongoDBURI, a.config.MongoDBName, a.config.MongoDBCollection)
87115
if err != nil {
116+
// If connection fails, log the error and fall back to in-memory storage
88117
log.Printf("Failed to connect to MongoDB: %v, falling back to in-memory storage", err)
89118
noteStorage = storage.NewInMemoryStorage()
90119
} else {
91120
log.Println("Successfully connected to MongoDB")
92121
}
93122
default:
123+
// Use in-memory storage by default
94124
log.Println("Using in-memory storage")
95125
noteStorage = storage.NewInMemoryStorage()
96126
}
97127

98128
return noteStorage, nil
99129
}
100130

101-
// setupRESTServer creates and configures the REST server
131+
// setupRESTServer creates and configures the REST API server.
132+
// It sets up:
133+
// 1. A new REST handler with the storage backend
134+
// 2. A Chi router with middleware for logging and panic recovery
135+
// 3. Routes for the REST API endpoints
136+
// 4. An HTTP server with the configured port
102137
func (a *App) setupRESTServer() *http.Server {
138+
// Create a new REST handler with the storage backend
103139
restHandler := rest.NewHandler(a.storage)
140+
141+
// Create a new Chi router
142+
// Chi is a lightweight, idiomatic and composable router for Go HTTP services
104143
r := chi.NewRouter()
105144

106-
// Middleware
107-
r.Use(middleware.Logger)
108-
r.Use(middleware.Recoverer)
145+
// Add middleware to the router
146+
r.Use(middleware.Logger) // Log all HTTP requests
147+
r.Use(middleware.Recoverer) // Recover from panics without crashing the server
109148

149+
// Register the API routes with the router
150+
// This sets up endpoints like GET /api/notes, POST /api/notes, etc.
110151
restHandler.RegisterRoutes(r)
111152

153+
// Create and return an HTTP server with the configured port and router
112154
return &http.Server{
113-
Addr: a.config.RESTPort,
114-
Handler: r,
155+
Addr: a.config.RESTPort, // Port to listen on (e.g., ":8080")
156+
Handler: r, // The router that handles requests
115157
}
116158
}
117159

118-
// setupGRPCServer creates and configures the gRPC server
160+
// setupGRPCServer creates and configures the gRPC server.
161+
// It extracts the port number from the configuration and creates a new gRPC server
162+
// with the storage backend and port.
119163
func (a *App) setupGRPCServer() *grpc.Server {
120-
// Remove the colon prefix and convert to int
121-
port := 8081 // Default port
164+
// Extract the port number from the configuration
165+
// The port might be in the format ":8081", so we need to remove the colon prefix
166+
port := 8081 // Default port if parsing fails
122167
if a.config.GRPCPort != "" {
123168
portStr := strings.TrimPrefix(a.config.GRPCPort, ":")
124169
if p, err := strconv.Atoi(portStr); err == nil {
125170
port = p
126171
}
127172
}
173+
174+
// Create and return a new gRPC server with the storage backend and port
128175
return grpc.NewServer(a.storage, port)
129176
}
130177

131-
// startServers starts the REST and gRPC servers in separate goroutines
178+
// startServers starts the REST and gRPC servers in separate goroutines.
179+
// This method doesn't block; it returns immediately after starting the servers.
180+
// Each server runs in its own goroutine (a lightweight thread) to allow them to run concurrently.
132181
func (a *App) startServers(ctx context.Context) error {
133-
// Start REST server
182+
// Start REST server in a separate goroutine
134183
go func() {
135184
log.Printf("Starting REST server on %s", a.config.RESTPort)
185+
// ListenAndServe blocks until the server is stopped or encounters an error
136186
if err := a.restServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
187+
// Log any error that isn't just the server being closed normally
137188
log.Printf("REST server failed: %v", err)
138189
}
139190
}()
140191

141-
// Start gRPC server
192+
// Start gRPC server in a separate goroutine
142193
go func() {
143194
log.Printf("Starting gRPC server on %s", a.config.GRPCPort)
195+
// Start blocks until the server is stopped or encounters an error
144196
if err := a.grpcServer.Start(); err != nil {
145197
log.Printf("gRPC server failed: %v", err)
146198
}
@@ -149,31 +201,40 @@ func (a *App) startServers(ctx context.Context) error {
149201
return nil
150202
}
151203

152-
// waitForShutdown waits for interrupt signal and gracefully shuts down the servers
204+
// waitForShutdown waits for the context to be canceled (e.g., by an interrupt signal)
205+
// and then gracefully shuts down the servers.
206+
// This method blocks until the context is canceled and the servers are shut down.
153207
func (a *App) waitForShutdown(ctx context.Context) error {
208+
// Block until the context is canceled (e.g., by Ctrl+C)
154209
<-ctx.Done()
155210
log.Println("Shutting down servers...")
156211

157-
// Create a new context with timeout for shutdown
212+
// Create a new context with a 5-second timeout for the shutdown process
213+
// This ensures that shutdown doesn't hang indefinitely
158214
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
159-
defer cancel()
215+
defer cancel() // Ensure the context is canceled when the function returns
160216

161-
// Shutdown the REST server
217+
// Gracefully shut down the REST server
218+
// This allows in-flight requests to complete before shutting down
162219
if err := a.restServer.Shutdown(shutdownCtx); err != nil {
163220
log.Printf("REST server shutdown failed: %v", err)
164221
}
165222

166-
// Close the storage
223+
// Close the storage connection
224+
// This ensures any database connections are properly closed
167225
if err := a.storage.Close(shutdownCtx); err != nil {
168226
log.Printf("Storage shutdown failed: %v", err)
169227
}
170228

171229
log.Println("Servers stopped")
230+
// Return the original context's error (typically context.Canceled)
172231
return ctx.Err()
173232
}
174233

175-
// createSampleNotes creates some sample notes in the storage
234+
// createSampleNotes creates some sample notes in the storage for demonstration purposes.
235+
// This provides initial data for users to see when they first access the API.
176236
func (a *App) createSampleNotes(ctx context.Context) error {
237+
// Define a list of sample notes to create
177238
notes := []struct {
178239
title string
179240
content string
@@ -192,41 +253,60 @@ func (a *App) createSampleNotes(ctx context.Context) error {
192253
},
193254
}
194255

256+
// Create each sample note in the storage
195257
for _, note := range notes {
258+
// Create a new Note object with the title and content
196259
n := model.NewNote(note.title, note.content)
260+
261+
// Try to save the note to the storage
197262
err := a.storage.Create(ctx, n)
198263
if err != nil {
199-
// Ignore duplicate key errors (MongoDB error code 11000)
264+
// If the note already exists (duplicate key error), skip it and continue
200265
if isDuplicateKeyError(err) {
201266
continue
202267
}
268+
// For any other error, return it
203269
return fmt.Errorf("failed to create sample note: %w", err)
204270
}
205-
// Add a small delay to ensure unique IDs when using timestamp-based ID generation
271+
272+
// Add a small delay between creating notes
273+
// This ensures unique IDs when using timestamp-based ID generation
274+
// (since our ID generation uses the current timestamp)
206275
time.Sleep(1 * time.Millisecond)
207276
}
208277

209278
return nil
210279
}
211280

212-
// isDuplicateKeyError checks if the error is a duplicate key error
281+
// isDuplicateKeyError checks if the error is a duplicate key error from any of the
282+
// supported storage backends (MongoDB, CouchDB, or in-memory).
283+
//
284+
// Different databases return different error messages for duplicate key errors,
285+
// so this function normalizes them to a single boolean result.
213286
func isDuplicateKeyError(err error) bool {
214287
if err == nil {
215288
return false
216289
}
217290
errStr := err.Error()
218291

219-
// MongoDB duplicate key error
292+
// Check for MongoDB duplicate key error
293+
// MongoDB returns error code E11000 for duplicate key errors
220294
if strings.Contains(errStr, "E11000 duplicate key error") {
221295
return true
222296
}
223-
// CouchDB duplicate key error
297+
298+
// Check for CouchDB duplicate key error
299+
// CouchDB returns "conflict" or "Document update conflict" for duplicate key errors
224300
if strings.Contains(errStr, "conflict") || strings.Contains(errStr, "Document update conflict") {
225301
return true
226302
}
227-
// In-memory storage duplicate key error
303+
304+
// Check for in-memory storage duplicate key error
305+
// Our in-memory implementation returns "note already exists" for duplicate key errors
228306
if strings.Contains(errStr, "note already exists") {
229307
return true
230308
}
309+
310+
// If none of the above patterns match, it's not a duplicate key error
231311
return false
232312
}

0 commit comments

Comments
 (0)