blob: 664471d12d3f8e449983cab90627143333041caf [file] [log] [blame]
Akron49ceeb42025-05-23 17:46:01 +02001package main
2
3import (
Akron49ceeb42025-05-23 17:46:01 +02004 "fmt"
5 "os"
6 "os/signal"
7 "strings"
8 "syscall"
9
Akronfa55bb22025-05-26 15:10:42 +020010 "github.com/KorAP/KoralPipe-TermMapper/mapper"
Akron1fc750e2025-05-26 16:54:18 +020011 "github.com/alecthomas/kong"
Akron49ceeb42025-05-23 17:46:01 +020012 "github.com/gofiber/fiber/v2"
13 "github.com/rs/zerolog"
14 "github.com/rs/zerolog/log"
15)
16
Akron74e1c072025-05-26 14:38:25 +020017const (
18 maxInputLength = 1024 * 1024 // 1MB
19 maxParamLength = 1024 // 1KB
20)
21
Akron49ceeb42025-05-23 17:46:01 +020022type config struct {
Akron1fc750e2025-05-26 16:54:18 +020023 Port int `kong:"short='p',default='8080',help='Port to listen on'"`
24 Config string `kong:"short='c',required,help='YAML configuration file containing mapping directives'"`
25 LogLevel string `kong:"short='l',default='info',help='Log level (debug, info, warn, error)'"`
Akron49ceeb42025-05-23 17:46:01 +020026}
27
Akron1fc750e2025-05-26 16:54:18 +020028func parseConfig() *config {
Akron49ceeb42025-05-23 17:46:01 +020029 cfg := &config{}
Akron1fc750e2025-05-26 16:54:18 +020030 ctx := kong.Parse(cfg,
31 kong.Description("A web service for transforming JSON objects using term mapping rules."),
32 kong.UsageOnError(),
33 )
34 if ctx.Error != nil {
35 fmt.Fprintln(os.Stderr, ctx.Error)
Akron49ceeb42025-05-23 17:46:01 +020036 os.Exit(1)
37 }
Akron49ceeb42025-05-23 17:46:01 +020038 return cfg
39}
40
41func setupLogger(level string) {
42 // Parse log level
43 lvl, err := zerolog.ParseLevel(strings.ToLower(level))
44 if err != nil {
45 log.Error().Err(err).Str("level", level).Msg("Invalid log level, defaulting to info")
46 lvl = zerolog.InfoLevel
47 }
48
49 // Configure zerolog
50 zerolog.SetGlobalLevel(lvl)
51 log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
52}
53
54func main() {
55 // Parse command line flags
Akron1fc750e2025-05-26 16:54:18 +020056 cfg := parseConfig()
Akron49ceeb42025-05-23 17:46:01 +020057
58 // Set up logging
Akron1fc750e2025-05-26 16:54:18 +020059 setupLogger(cfg.LogLevel)
Akron49ceeb42025-05-23 17:46:01 +020060
61 // Create a new mapper instance
Akron1fc750e2025-05-26 16:54:18 +020062 m, err := mapper.NewMapper(cfg.Config)
Akron49ceeb42025-05-23 17:46:01 +020063 if err != nil {
64 log.Fatal().Err(err).Msg("Failed to create mapper")
65 }
66
67 // Create fiber app
68 app := fiber.New(fiber.Config{
69 DisableStartupMessage: true,
Akron74e1c072025-05-26 14:38:25 +020070 BodyLimit: maxInputLength,
Akron49ceeb42025-05-23 17:46:01 +020071 })
72
73 // Set up routes
74 setupRoutes(app, m)
75
76 // Start server
77 go func() {
Akron1fc750e2025-05-26 16:54:18 +020078 log.Info().Int("port", cfg.Port).Msg("Starting server")
79 if err := app.Listen(fmt.Sprintf(":%d", cfg.Port)); err != nil {
Akron49ceeb42025-05-23 17:46:01 +020080 log.Fatal().Err(err).Msg("Server error")
81 }
82 }()
83
84 // Wait for interrupt signal
85 sigChan := make(chan os.Signal, 1)
86 signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
87 <-sigChan
88
89 // Graceful shutdown
90 log.Info().Msg("Shutting down server")
91 if err := app.Shutdown(); err != nil {
92 log.Error().Err(err).Msg("Error during shutdown")
93 }
94}
95
96func setupRoutes(app *fiber.App, m *mapper.Mapper) {
97 // Health check endpoint
98 app.Get("/health", func(c *fiber.Ctx) error {
99 return c.SendString("OK")
100 })
101
102 // Transformation endpoint
103 app.Post("/:map/query", handleTransform(m))
104}
105
106func handleTransform(m *mapper.Mapper) fiber.Handler {
107 return func(c *fiber.Ctx) error {
108 // Get parameters
109 mapID := c.Params("map")
110 dir := c.Query("dir", "atob")
111 foundryA := c.Query("foundryA", "")
112 foundryB := c.Query("foundryB", "")
113 layerA := c.Query("layerA", "")
114 layerB := c.Query("layerB", "")
115
Akron74e1c072025-05-26 14:38:25 +0200116 // Validate input parameters
117 if err := validateInput(mapID, dir, foundryA, foundryB, layerA, layerB, c.Body()); err != nil {
118 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
119 "error": err.Error(),
120 })
121 }
122
Akron49ceeb42025-05-23 17:46:01 +0200123 // Validate direction
124 if dir != "atob" && dir != "btoa" {
125 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
126 "error": "invalid direction, must be 'atob' or 'btoa'",
127 })
128 }
129
130 // Parse request body
Akron2cbdab52025-05-23 17:57:10 +0200131 var jsonData any
Akron49ceeb42025-05-23 17:46:01 +0200132 if err := c.BodyParser(&jsonData); err != nil {
133 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
134 "error": "invalid JSON in request body",
135 })
136 }
137
138 // Apply mappings
139 result, err := m.ApplyMappings(mapID, mapper.MappingOptions{
140 Direction: mapper.Direction(dir),
141 FoundryA: foundryA,
142 FoundryB: foundryB,
143 LayerA: layerA,
144 LayerB: layerB,
145 }, jsonData)
146
147 if err != nil {
148 log.Error().Err(err).
149 Str("mapID", mapID).
150 Str("direction", dir).
151 Msg("Failed to apply mappings")
152
153 return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
154 "error": err.Error(),
155 })
156 }
157
158 return c.JSON(result)
159 }
160}
Akron74e1c072025-05-26 14:38:25 +0200161
162// validateInput checks if the input parameters are valid
163func validateInput(mapID, dir, foundryA, foundryB, layerA, layerB string, body []byte) error {
Akron69d43bf2025-05-26 17:09:00 +0200164 // Define parameter checks
165 params := []struct {
Akron74e1c072025-05-26 14:38:25 +0200166 name string
167 value string
168 }{
169 {"mapID", mapID},
170 {"dir", dir},
171 {"foundryA", foundryA},
172 {"foundryB", foundryB},
173 {"layerA", layerA},
174 {"layerB", layerB},
Akron69d43bf2025-05-26 17:09:00 +0200175 }
176
177 for _, param := range params {
178 // Check input lengths
179 if len(param.value) > maxParamLength {
180 return fmt.Errorf("%s too long (max %d bytes)", param.name, maxParamLength)
181 }
182 // Check for invalid characters in parameters
Akron74e1c072025-05-26 14:38:25 +0200183 if strings.ContainsAny(param.value, "<>{}[]\\") {
184 return fmt.Errorf("%s contains invalid characters", param.name)
185 }
186 }
187
Akron69d43bf2025-05-26 17:09:00 +0200188 if len(body) > maxInputLength {
189 return fmt.Errorf("request body too large (max %d bytes)", maxInputLength)
190 }
191
Akron74e1c072025-05-26 14:38:25 +0200192 return nil
193}