blob: 0f619c18770f33e54bce482c40f5c9e9f3b026e4 [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 {
164 // Check input lengths
165 if len(mapID) > maxParamLength {
166 return fmt.Errorf("map ID too long (max %d bytes)", maxParamLength)
167 }
168 if len(dir) > maxParamLength {
169 return fmt.Errorf("direction too long (max %d bytes)", maxParamLength)
170 }
171 if len(foundryA) > maxParamLength {
172 return fmt.Errorf("foundryA too long (max %d bytes)", maxParamLength)
173 }
174 if len(foundryB) > maxParamLength {
175 return fmt.Errorf("foundryB too long (max %d bytes)", maxParamLength)
176 }
177 if len(layerA) > maxParamLength {
178 return fmt.Errorf("layerA too long (max %d bytes)", maxParamLength)
179 }
180 if len(layerB) > maxParamLength {
181 return fmt.Errorf("layerB too long (max %d bytes)", maxParamLength)
182 }
183 if len(body) > maxInputLength {
184 return fmt.Errorf("request body too large (max %d bytes)", maxInputLength)
185 }
186
187 // Check for invalid characters in parameters
188 for _, param := range []struct {
189 name string
190 value string
191 }{
192 {"mapID", mapID},
193 {"dir", dir},
194 {"foundryA", foundryA},
195 {"foundryB", foundryB},
196 {"layerA", layerA},
197 {"layerB", layerB},
198 } {
199 if strings.ContainsAny(param.value, "<>{}[]\\") {
200 return fmt.Errorf("%s contains invalid characters", param.name)
201 }
202 }
203
204 return nil
205}