Getting Started Examples
This page provides practical examples to help you get familiar with Saturn CLI concepts and usage patterns.
Simple Job Example
Let's start with a basic job registration and execution:
// server.go
package main
import (
"fmt"
"log"
"github.com/Kingson4Wu/saturncli/server"
"github.com/Kingson4Wu/saturncli/utils"
)
func main() {
// Register a simple job
if err := server.AddJob("echo", func(args map[string]string, signature string) bool {
message := args["message"]
fmt.Printf("[%s] Echo: %s\n", signature, message)
return true
}); err != nil {
log.Fatal("Failed to register job:", err)
}
fmt.Println("Server running on /tmp/echo.sock")
server.NewServer(&utils.DefaultLogger{}, "/tmp/echo.sock").Serve()
}
Execute with:
go run server.go
In another terminal:
./saturn_cli --name echo --param message="Hello, World!"
Stoppable Job with Progress
A more advanced example showing a job that reports progress and can be stopped:
// progress_server.go
package main
import (
"fmt"
"time"
"github.com/Kingson4Wu/saturncli/server"
"github.com/Kingson4Wu/saturncli/utils"
)
func main() {
if err := server.AddStoppableJob("progress-task", func(args map[string]string, signature string, quit chan struct{}) bool {
steps := 20
if args["steps"] != "" {
fmt.Sscanf(args["steps"], "%d", &steps)
}
fmt.Printf("[%s] Starting task with %d steps\n", signature, steps)
completed := 0
for i := 1; i <= steps; i++ {
select {
case <-quit:
fmt.Printf("[%s] Task cancelled at step %d of %d (completed %d)\n",
signature, i, steps, completed)
return true
case <-time.After(200 * time.Millisecond): // Simulate work
completed = i
if i%5 == 0 { // Report progress every 5 steps
fmt.Printf("[%s] Progress: %d/%d steps completed\n", signature, i, steps)
}
}
}
fmt.Printf("[%s] Task completed successfully (%d steps)\n", signature, steps)
return true
}); err != nil {
panic(err)
}
server.NewServer(&utils.DefaultLogger{}, "/tmp/progress.sock").Serve()
}
Test it:
# Start the server
go run progress_server.go
# In another terminal, start the task
./saturn_cli --name progress-task --param steps=30
# In a third terminal, stop the task before completion
./saturn_cli --name progress-task --stop
File Processing Example
Here's a practical example of processing files with error handling:
// file_processor.go
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/Kingson4Wu/saturncli/server"
"github.com/Kingson4Wu/saturncli/utils"
)
func main() {
if err := server.AddStoppableJob("file-processor", func(args map[string]string, signature string, quit chan struct{}) bool {
inputDir := args["input_dir"]
outputDir := args["output_dir"]
if inputDir == "" || outputDir == "" {
fmt.Printf("[%s] ERROR: input_dir and output_dir are required\n", signature)
return false
}
// Verify directories exist
if _, err := os.Stat(inputDir); os.IsNotExist(err) {
fmt.Printf("[%s] ERROR: Input directory does not exist: %s\n", signature, inputDir)
return false
}
if err := os.MkdirAll(outputDir, 0755); err != nil {
fmt.Printf("[%s] ERROR: Failed to create output directory: %v\n", signature, err)
return false
}
fmt.Printf("[%s] Processing files from %s to %s\n", signature, inputDir, outputDir)
// Find all .txt files
files, err := filepath.Glob(filepath.Join(inputDir, "*.txt"))
if err != nil {
fmt.Printf("[%s] ERROR: Failed to find files: %v\n", signature, err)
return false
}
processed := 0
for _, file := range files {
select {
case <-quit:
fmt.Printf("[%s] Processing cancelled, %d files processed\n", signature, processed)
return true
default:
if processFile(file, outputDir, signature) {
processed++
fmt.Printf("[%s] Processed: %s\n", signature, filepath.Base(file))
}
}
}
fmt.Printf("[%s] Completed processing %d files\n", signature, processed)
return true
}); err != nil {
panic(err)
}
server.NewServer(&utils.DefaultLogger{}, "/tmp/file-processor.sock").Serve()
}
func processFile(inputPath, outputDir, signature string) bool {
// Simulate file processing (in real implementation, you'd read, process, and write the file)
time.Sleep(500 * time.Millisecond) // Simulate processing time
filename := filepath.Base(inputPath)
nameWithoutExt := strings.TrimSuffix(filename, filepath.Ext(filename))
outputPath := filepath.Join(outputDir, fmt.Sprintf("%s_processed.txt", nameWithoutExt))
// In a real implementation, you would actually process the file content
fmt.Printf("[%s] Would write processed content to: %s\n", signature, outputPath)
return true
}
Embedding in a Web Service
This example shows how to embed Saturn CLI in a web service:
// web_service.go
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/Kingson4Wu/saturncli/client"
"github.com/Kingson4Wu/saturncli/base"
"github.com/Kingson4Wu/saturncli/server"
"github.com/Kingson4Wu/saturncli/utils"
)
type WebService struct {
saturnClient *client.cli
}
type JobRequest struct {
Name string `json:"name"`
Params map[string]string `json:"params"`
}
type JobResponse struct {
Success bool `json:"success"`
Result string `json:"result"`
}
func NewWebService() *WebService {
return &WebService{
saturnClient: client.NewClient(&utils.DefaultLogger{}, "/tmp/web_service.sock"),
}
}
func (ws *WebService) JobHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req JobRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
result := ws.saturnClient.Run(&client.Task{
Name: req.Name,
Params: req.Params,
})
response := JobResponse{
Success: result == base.SUCCESS,
Result: string(result),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
func main() {
// Set up Saturn jobs
if err := server.AddJob("data-processor", func(args map[string]string, signature string) bool {
fmt.Printf("[%s] Processing data with args: %+v\n", signature, args)
// Simulate processing
// In real implementation, perform actual data processing
return true
}); err != nil {
log.Fatal("Failed to register job:", err)
}
// Start Saturn server in a goroutine
go func() {
fmt.Println("Starting Saturn server...")
server.NewServer(&utils.DefaultLogger{}, "/tmp/web_service.sock").Serve()
}()
// Give server time to start
time.Sleep(100 * time.Millisecond)
// Set up web API
webService := NewWebService()
http.HandleFunc("/job", webService.JobHandler)
fmt.Println("Starting web service on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Testing Your Examples
Create a simple test for your Saturn jobs:
// job_test.go
package main
import (
"bytes"
"fmt"
"strings"
"testing"
"time"
)
type TestLogger struct {
buffer *bytes.Buffer
}
func (tl *TestLogger) Info(msg string) { tl.buffer.WriteString("INFO: " + msg + "\n") }
func (tl *TestLogger) Infof(format string, args ...interface{}) { tl.buffer.WriteString("INFO: " + fmt.Sprintf(format, args...) + "\n") }
func (tl *TestLogger) Error(msg string) { tl.buffer.WriteString("ERROR: " + msg + "\n") }
func (tl *TestLogger) Errorf(format string, args ...interface{}) { tl.buffer.WriteString("ERROR: " + fmt.Sprintf(format, args...) + "\n") }
func (tl *TestLogger) Debug(msg string) { tl.buffer.WriteString("DEBUG: " + msg + "\n") }
func (tl *TestLogger) Debugf(format string, args ...interface{}) { tl.buffer.WriteString("DEBUG: " + fmt.Sprintf(format, args...) + "\n") }
func (tl *TestLogger) Warn(msg string) { tl.buffer.WriteString("WARN: " + msg + "\n") }
func (tl *TestLogger) Warnf(format string, args ...interface{}) { tl.buffer.WriteString("WARN: " + fmt.Sprintf(format, args...) + "\n") }
func TestEchoJob(t *testing.T) {
var logBuffer bytes.Buffer
testLogger := &TestLogger{buffer: &logBuffer}
// Test parameters
args := map[string]string{"message": "Test message"}
signature := "test-signature"
// Create job handler function
handler := func(jobArgs map[string]string, jobSignature string) bool {
message := jobArgs["message"]
testLogger.Infof("Echo: %s (Signature: %s)", message, jobSignature)
return true
}
// Execute the job
result := handler(args, signature)
if !result {
t.Error("Expected job to return true")
}
logOutput := logBuffer.String()
if !strings.Contains(logOutput, "Test message") {
t.Errorf("Expected log to contain 'Test message', got: %s", logOutput)
}
if !strings.Contains(logOutput, "test-signature") {
t.Errorf("Expected log to contain signature, got: %s", logOutput)
}
}
func TestStoppableJob(t *testing.T) {
quit := make(chan struct{})
done := make(chan bool, 1)
handler := func(args map[string]string, signature string, quitChan chan struct{}) bool {
iterations := 0
maxIterations := 10
for iterations < maxIterations {
select {
case <-quitChan:
return true
default:
iterations++
time.Sleep(10 * time.Millisecond) // Small sleep for the test
}
}
return true
}
args := map[string]string{}
signature := "test-stop"
// Run the handler in a goroutine
go func() {
result := handler(args, signature, quit)
done <- result
}()
// Allow some iterations
time.Sleep(50 * time.Millisecond)
// Close the quit channel to stop the job
close(quit)
// Wait for the handler to finish
select {
case result := <-done:
if !result {
t.Error("Expected stoppable job to return true after being stopped")
}
case <-time.After(1 * time.Second):
t.Error("Test timed out")
}
}
Running Examples
To run any of these examples:
- Create a new directory for the example
- Create the Go file with the example code
- Run
go mod init example_name - Run
go get github.com/Kingson4Wu/saturncli - Build with
go build -o example_name main.go - Run the server:
./example_name - In another terminal, use the Saturn CLI or create a client to trigger jobs
Source Code References
Next Steps
- Review the API Reference for detailed function documentation
- Check out the Embedding Guide for more integration patterns
- Look at the Architecture to understand the system design