Added web server for transformations
diff --git a/cmd/termmapper/main.go b/cmd/termmapper/main.go
new file mode 100644
index 0000000..aa4e3e2
--- /dev/null
+++ b/cmd/termmapper/main.go
@@ -0,0 +1,164 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"os"
+	"os/signal"
+	"strings"
+	"syscall"
+
+	"github.com/KorAP/KoralPipe-TermMapper2/pkg/mapper"
+	"github.com/gofiber/fiber/v2"
+	"github.com/rs/zerolog"
+	"github.com/rs/zerolog/log"
+)
+
+type config struct {
+	port     int
+	config   string
+	logLevel string
+}
+
+func parseFlags() *config {
+	cfg := &config{}
+
+	flag.IntVar(&cfg.port, "port", 8080, "Port to listen on")
+	flag.IntVar(&cfg.port, "p", 8080, "Port to listen on (shorthand)")
+
+	flag.StringVar(&cfg.config, "config", "", "YAML configuration file containing mapping directives")
+	flag.StringVar(&cfg.config, "c", "", "YAML configuration file containing mapping directives (shorthand)")
+
+	flag.StringVar(&cfg.logLevel, "log-level", "info", "Log level (debug, info, warn, error)")
+	flag.StringVar(&cfg.logLevel, "l", "info", "Log level (shorthand)")
+
+	flag.Usage = func() {
+		fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
+		fmt.Fprintf(os.Stderr, "\nA web service for transforming JSON objects using term mapping rules.\n\n")
+		fmt.Fprintf(os.Stderr, "Options:\n")
+		flag.PrintDefaults()
+	}
+
+	flag.Parse()
+
+	if cfg.config == "" {
+		fmt.Fprintln(os.Stderr, "Error: config file is required")
+		flag.Usage()
+		os.Exit(1)
+	}
+
+	return cfg
+}
+
+func setupLogger(level string) {
+	// Parse log level
+	lvl, err := zerolog.ParseLevel(strings.ToLower(level))
+	if err != nil {
+		log.Error().Err(err).Str("level", level).Msg("Invalid log level, defaulting to info")
+		lvl = zerolog.InfoLevel
+	}
+
+	// Configure zerolog
+	zerolog.SetGlobalLevel(lvl)
+	log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
+}
+
+func main() {
+	// Parse command line flags
+	cfg := parseFlags()
+
+	// Set up logging
+	setupLogger(cfg.logLevel)
+
+	// Create a new mapper instance
+	m, err := mapper.NewMapper(cfg.config)
+	if err != nil {
+		log.Fatal().Err(err).Msg("Failed to create mapper")
+	}
+
+	// Create fiber app
+	app := fiber.New(fiber.Config{
+		DisableStartupMessage: true,
+	})
+
+	// Set up routes
+	setupRoutes(app, m)
+
+	// Start server
+	go func() {
+		log.Info().Int("port", cfg.port).Msg("Starting server")
+		if err := app.Listen(fmt.Sprintf(":%d", cfg.port)); err != nil {
+			log.Fatal().Err(err).Msg("Server error")
+		}
+	}()
+
+	// Wait for interrupt signal
+	sigChan := make(chan os.Signal, 1)
+	signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
+	<-sigChan
+
+	// Graceful shutdown
+	log.Info().Msg("Shutting down server")
+	if err := app.Shutdown(); err != nil {
+		log.Error().Err(err).Msg("Error during shutdown")
+	}
+}
+
+func setupRoutes(app *fiber.App, m *mapper.Mapper) {
+	// Health check endpoint
+	app.Get("/health", func(c *fiber.Ctx) error {
+		return c.SendString("OK")
+	})
+
+	// Transformation endpoint
+	app.Post("/:map/query", handleTransform(m))
+}
+
+func handleTransform(m *mapper.Mapper) fiber.Handler {
+	return func(c *fiber.Ctx) error {
+		// Get parameters
+		mapID := c.Params("map")
+		dir := c.Query("dir", "atob")
+		foundryA := c.Query("foundryA", "")
+		foundryB := c.Query("foundryB", "")
+		layerA := c.Query("layerA", "")
+		layerB := c.Query("layerB", "")
+
+		// Validate direction
+		if dir != "atob" && dir != "btoa" {
+			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+				"error": "invalid direction, must be 'atob' or 'btoa'",
+			})
+		}
+
+		// Parse request body
+		var jsonData interface{}
+		if err := c.BodyParser(&jsonData); err != nil {
+			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+				"error": "invalid JSON in request body",
+			})
+		}
+
+		// Apply mappings
+		result, err := m.ApplyMappings(mapID, mapper.MappingOptions{
+			Direction: mapper.Direction(dir),
+			FoundryA:  foundryA,
+			FoundryB:  foundryB,
+			LayerA:    layerA,
+			LayerB:    layerB,
+		}, jsonData)
+
+		if err != nil {
+			log.Error().Err(err).
+				Str("mapID", mapID).
+				Str("direction", dir).
+				Msg("Failed to apply mappings")
+
+			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
+				"error": err.Error(),
+			})
+		}
+
+		return c.JSON(result)
+	}
+}