blob: 9d45bbc4141d608e175a9740d3024d996a0b60b0 [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
Akrona00d4752025-05-26 17:34:36 +020010 "github.com/KorAP/KoralPipe-TermMapper/config"
Akronfa55bb22025-05-26 15:10:42 +020011 "github.com/KorAP/KoralPipe-TermMapper/mapper"
Akron1fc750e2025-05-26 16:54:18 +020012 "github.com/alecthomas/kong"
Akron49ceeb42025-05-23 17:46:01 +020013 "github.com/gofiber/fiber/v2"
14 "github.com/rs/zerolog"
15 "github.com/rs/zerolog/log"
16)
17
Akron74e1c072025-05-26 14:38:25 +020018const (
19 maxInputLength = 1024 * 1024 // 1MB
20 maxParamLength = 1024 // 1KB
21)
22
Akrona00d4752025-05-26 17:34:36 +020023type appConfig struct {
Akron1fc750e2025-05-26 16:54:18 +020024 Port int `kong:"short='p',default='8080',help='Port to listen on'"`
25 Config string `kong:"short='c',required,help='YAML configuration file containing mapping directives'"`
26 LogLevel string `kong:"short='l',default='info',help='Log level (debug, info, warn, error)'"`
Akron49ceeb42025-05-23 17:46:01 +020027}
28
Akrona00d4752025-05-26 17:34:36 +020029func parseConfig() *appConfig {
30 cfg := &appConfig{}
Akron1fc750e2025-05-26 16:54:18 +020031 ctx := kong.Parse(cfg,
32 kong.Description("A web service for transforming JSON objects using term mapping rules."),
33 kong.UsageOnError(),
34 )
35 if ctx.Error != nil {
36 fmt.Fprintln(os.Stderr, ctx.Error)
Akron49ceeb42025-05-23 17:46:01 +020037 os.Exit(1)
38 }
Akron49ceeb42025-05-23 17:46:01 +020039 return cfg
40}
41
42func setupLogger(level string) {
43 // Parse log level
44 lvl, err := zerolog.ParseLevel(strings.ToLower(level))
45 if err != nil {
46 log.Error().Err(err).Str("level", level).Msg("Invalid log level, defaulting to info")
47 lvl = zerolog.InfoLevel
48 }
49
50 // Configure zerolog
51 zerolog.SetGlobalLevel(lvl)
52 log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
53}
54
55func main() {
56 // Parse command line flags
Akron1fc750e2025-05-26 16:54:18 +020057 cfg := parseConfig()
Akron49ceeb42025-05-23 17:46:01 +020058
59 // Set up logging
Akron1fc750e2025-05-26 16:54:18 +020060 setupLogger(cfg.LogLevel)
Akron49ceeb42025-05-23 17:46:01 +020061
Akrona00d4752025-05-26 17:34:36 +020062 // Load configuration file
63 yamlConfig, err := config.LoadConfig(cfg.Config)
64 if err != nil {
65 log.Fatal().Err(err).Msg("Failed to load configuration")
66 }
67
Akron49ceeb42025-05-23 17:46:01 +020068 // Create a new mapper instance
Akrona00d4752025-05-26 17:34:36 +020069 m, err := mapper.NewMapper(yamlConfig.Lists)
Akron49ceeb42025-05-23 17:46:01 +020070 if err != nil {
71 log.Fatal().Err(err).Msg("Failed to create mapper")
72 }
73
74 // Create fiber app
75 app := fiber.New(fiber.Config{
76 DisableStartupMessage: true,
Akron74e1c072025-05-26 14:38:25 +020077 BodyLimit: maxInputLength,
Akron49ceeb42025-05-23 17:46:01 +020078 })
79
80 // Set up routes
81 setupRoutes(app, m)
82
83 // Start server
84 go func() {
Akron1fc750e2025-05-26 16:54:18 +020085 log.Info().Int("port", cfg.Port).Msg("Starting server")
86 if err := app.Listen(fmt.Sprintf(":%d", cfg.Port)); err != nil {
Akron49ceeb42025-05-23 17:46:01 +020087 log.Fatal().Err(err).Msg("Server error")
88 }
89 }()
90
91 // Wait for interrupt signal
92 sigChan := make(chan os.Signal, 1)
93 signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
94 <-sigChan
95
96 // Graceful shutdown
97 log.Info().Msg("Shutting down server")
98 if err := app.Shutdown(); err != nil {
99 log.Error().Err(err).Msg("Error during shutdown")
100 }
101}
102
103func setupRoutes(app *fiber.App, m *mapper.Mapper) {
104 // Health check endpoint
105 app.Get("/health", func(c *fiber.Ctx) error {
106 return c.SendString("OK")
107 })
108
109 // Transformation endpoint
110 app.Post("/:map/query", handleTransform(m))
111}
112
113func handleTransform(m *mapper.Mapper) fiber.Handler {
114 return func(c *fiber.Ctx) error {
115 // Get parameters
116 mapID := c.Params("map")
117 dir := c.Query("dir", "atob")
118 foundryA := c.Query("foundryA", "")
119 foundryB := c.Query("foundryB", "")
120 layerA := c.Query("layerA", "")
121 layerB := c.Query("layerB", "")
122
Akron74e1c072025-05-26 14:38:25 +0200123 // Validate input parameters
124 if err := validateInput(mapID, dir, foundryA, foundryB, layerA, layerB, c.Body()); err != nil {
125 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
126 "error": err.Error(),
127 })
128 }
129
Akron49ceeb42025-05-23 17:46:01 +0200130 // Validate direction
131 if dir != "atob" && dir != "btoa" {
132 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
133 "error": "invalid direction, must be 'atob' or 'btoa'",
134 })
135 }
136
137 // Parse request body
Akron2cbdab52025-05-23 17:57:10 +0200138 var jsonData any
Akron49ceeb42025-05-23 17:46:01 +0200139 if err := c.BodyParser(&jsonData); err != nil {
140 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
141 "error": "invalid JSON in request body",
142 })
143 }
144
Akrona1a183f2025-05-26 17:47:33 +0200145 // Parse direction
146 direction, err := mapper.ParseDirection(dir)
147 if err != nil {
148 return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
149 "error": err.Error(),
150 })
151 }
152
Akron49ceeb42025-05-23 17:46:01 +0200153 // Apply mappings
Akron7b4984e2025-05-26 19:12:20 +0200154 result, err := m.ApplyQueryMappings(mapID, mapper.MappingOptions{
Akrona1a183f2025-05-26 17:47:33 +0200155 Direction: direction,
Akron49ceeb42025-05-23 17:46:01 +0200156 FoundryA: foundryA,
157 FoundryB: foundryB,
158 LayerA: layerA,
159 LayerB: layerB,
160 }, jsonData)
161
162 if err != nil {
163 log.Error().Err(err).
164 Str("mapID", mapID).
165 Str("direction", dir).
166 Msg("Failed to apply mappings")
167
168 return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
169 "error": err.Error(),
170 })
171 }
172
173 return c.JSON(result)
174 }
175}
Akron74e1c072025-05-26 14:38:25 +0200176
177// validateInput checks if the input parameters are valid
178func validateInput(mapID, dir, foundryA, foundryB, layerA, layerB string, body []byte) error {
Akron69d43bf2025-05-26 17:09:00 +0200179 // Define parameter checks
180 params := []struct {
Akron74e1c072025-05-26 14:38:25 +0200181 name string
182 value string
183 }{
184 {"mapID", mapID},
185 {"dir", dir},
186 {"foundryA", foundryA},
187 {"foundryB", foundryB},
188 {"layerA", layerA},
189 {"layerB", layerB},
Akron69d43bf2025-05-26 17:09:00 +0200190 }
191
192 for _, param := range params {
193 // Check input lengths
194 if len(param.value) > maxParamLength {
195 return fmt.Errorf("%s too long (max %d bytes)", param.name, maxParamLength)
196 }
197 // Check for invalid characters in parameters
Akron74e1c072025-05-26 14:38:25 +0200198 if strings.ContainsAny(param.value, "<>{}[]\\") {
199 return fmt.Errorf("%s contains invalid characters", param.name)
200 }
201 }
202
Akron69d43bf2025-05-26 17:09:00 +0200203 if len(body) > maxInputLength {
204 return fmt.Errorf("request body too large (max %d bytes)", maxInputLength)
205 }
206
Akron74e1c072025-05-26 14:38:25 +0200207 return nil
208}