Support loading multiple mapping files
Change-Id: I3c6caaa3c4c3434dfacfb842f0407250b5e980f0
diff --git a/cmd/termmapper/main.go b/cmd/termmapper/main.go
index ec8b93d..2a2f7e3 100644
--- a/cmd/termmapper/main.go
+++ b/cmd/termmapper/main.go
@@ -21,9 +21,10 @@
)
type appConfig struct {
- Port int `kong:"short='p',default='8080',help='Port to listen on'"`
- Config string `kong:"short='c',required,help='YAML configuration file containing mapping directives'"`
- LogLevel string `kong:"short='l',default='info',help='Log level (debug, info, warn, error)'"`
+ Port int `kong:"short='p',default='8080',help='Port to listen on'"`
+ Config string `kong:"short='c',help='YAML configuration file containing mapping directives and global settings'"`
+ Mappings []string `kong:"short='m',help='Individual YAML mapping files to load'"`
+ LogLevel string `kong:"short='l',default='info',help='Log level (debug, info, warn, error)'"`
}
// TemplateData holds data for the Kalamar plugin template
@@ -73,11 +74,16 @@
// Parse command line flags
cfg := parseConfig()
+ // Validate command line arguments
+ if cfg.Config == "" && len(cfg.Mappings) == 0 {
+ log.Fatal().Msg("At least one configuration source must be provided: use -c for main config file or -m for mapping files")
+ }
+
// Set up logging
setupLogger(cfg.LogLevel)
- // Load configuration file
- yamlConfig, err := config.LoadConfig(cfg.Config)
+ // Load configuration from multiple sources
+ yamlConfig, err := config.LoadFromSources(cfg.Config, cfg.Mappings)
if err != nil {
log.Fatal().Err(err).Msg("Failed to load configuration")
}
@@ -293,8 +299,8 @@
<dt><tt><strong>GET</strong> /:map</tt></dt>
<dd><small>Kalamar integration</small></dd>
-
- <dt><tt><strong>POST</strong> /:map/query</tt></dt>
+
+ <dt><tt><strong>POST</strong> /:map/query</tt></dt>
<dd><small>Transform JSON query objects using term mapping rules</small></dd>
</dl>
diff --git a/cmd/termmapper/main_test.go b/cmd/termmapper/main_test.go
index 292d3d9..c045e0b 100644
--- a/cmd/termmapper/main_test.go
+++ b/cmd/termmapper/main_test.go
@@ -6,6 +6,7 @@
"io"
"net/http"
"net/http/httptest"
+ "os"
"testing"
tmconfig "github.com/KorAP/KoralPipe-TermMapper/config"
@@ -385,3 +386,192 @@
})
}
}
+
+func TestMultipleMappingFiles(t *testing.T) {
+ // Create test mapping files
+ mappingFile1Content := `
+id: test-mapper-1
+foundryA: opennlp
+layerA: p
+foundryB: upos
+layerB: p
+mappings:
+ - "[PIDAT] <> [DET & AdjType=Pdt]"
+ - "[PAV] <> [ADV & PronType=Dem]"
+`
+ mappingFile1, err := os.CreateTemp("", "mapping1-*.yaml")
+ require.NoError(t, err)
+ defer os.Remove(mappingFile1.Name())
+
+ _, err = mappingFile1.WriteString(mappingFile1Content)
+ require.NoError(t, err)
+ err = mappingFile1.Close()
+ require.NoError(t, err)
+
+ mappingFile2Content := `
+id: test-mapper-2
+foundryA: stts
+layerA: p
+foundryB: upos
+layerB: p
+mappings:
+ - "[DET] <> [PRON]"
+ - "[ADJ] <> [NOUN]"
+`
+ mappingFile2, err := os.CreateTemp("", "mapping2-*.yaml")
+ require.NoError(t, err)
+ defer os.Remove(mappingFile2.Name())
+
+ _, err = mappingFile2.WriteString(mappingFile2Content)
+ require.NoError(t, err)
+ err = mappingFile2.Close()
+ require.NoError(t, err)
+
+ // Load configuration using multiple mapping files
+ config, err := tmconfig.LoadFromSources("", []string{mappingFile1.Name(), mappingFile2.Name()})
+ require.NoError(t, err)
+
+ // Create mapper
+ m, err := mapper.NewMapper(config.Lists)
+ require.NoError(t, err)
+
+ // Create fiber app
+ app := fiber.New()
+ setupRoutes(app, m, config)
+
+ // Test that both mappers work
+ testCases := []struct {
+ name string
+ mapID string
+ input string
+ expectGroup bool
+ expectedKey string
+ }{
+ {
+ name: "test-mapper-1 with complex mapping",
+ mapID: "test-mapper-1",
+ input: `{
+ "@type": "koral:token",
+ "wrap": {
+ "@type": "koral:term",
+ "foundry": "opennlp",
+ "key": "PIDAT",
+ "layer": "p",
+ "match": "match:eq"
+ }
+ }`,
+ expectGroup: true, // This mapping creates a termGroup because of "&"
+ expectedKey: "DET", // The first operand should be DET
+ },
+ {
+ name: "test-mapper-2 with simple mapping",
+ mapID: "test-mapper-2",
+ input: `{
+ "@type": "koral:token",
+ "wrap": {
+ "@type": "koral:term",
+ "foundry": "stts",
+ "key": "DET",
+ "layer": "p",
+ "match": "match:eq"
+ }
+ }`,
+ expectGroup: false, // This mapping creates a simple term
+ expectedKey: "PRON",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ req := httptest.NewRequest(http.MethodPost, "/"+tc.mapID+"/query?dir=atob", bytes.NewBufferString(tc.input))
+ req.Header.Set("Content-Type", "application/json")
+
+ resp, err := app.Test(req)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
+
+ var result map[string]interface{}
+ err = json.NewDecoder(resp.Body).Decode(&result)
+ require.NoError(t, err)
+
+ // Check that the mapping was applied
+ wrap := result["wrap"].(map[string]interface{})
+ if tc.expectGroup {
+ // For complex mappings, check the first operand
+ assert.Equal(t, "koral:termGroup", wrap["@type"])
+ operands := wrap["operands"].([]interface{})
+ require.Greater(t, len(operands), 0)
+ firstOperand := operands[0].(map[string]interface{})
+ assert.Equal(t, tc.expectedKey, firstOperand["key"])
+ } else {
+ // For simple mappings, check the key directly
+ assert.Equal(t, "koral:term", wrap["@type"])
+ assert.Equal(t, tc.expectedKey, wrap["key"])
+ }
+ })
+ }
+}
+
+func TestCombinedConfigAndMappingFiles(t *testing.T) {
+ // Create main config file
+ mainConfigContent := `
+sdk: "https://custom.example.com/sdk.js"
+server: "https://custom.example.com/"
+lists:
+- id: main-mapper
+ foundryA: opennlp
+ layerA: p
+ mappings:
+ - "[A] <> [B]"
+`
+ mainConfigFile, err := os.CreateTemp("", "main-config-*.yaml")
+ require.NoError(t, err)
+ defer os.Remove(mainConfigFile.Name())
+
+ _, err = mainConfigFile.WriteString(mainConfigContent)
+ require.NoError(t, err)
+ err = mainConfigFile.Close()
+ require.NoError(t, err)
+
+ // Create individual mapping file
+ mappingFileContent := `
+id: additional-mapper
+foundryA: stts
+layerA: p
+mappings:
+ - "[C] <> [D]"
+`
+ mappingFile, err := os.CreateTemp("", "mapping-*.yaml")
+ require.NoError(t, err)
+ defer os.Remove(mappingFile.Name())
+
+ _, err = mappingFile.WriteString(mappingFileContent)
+ require.NoError(t, err)
+ err = mappingFile.Close()
+ require.NoError(t, err)
+
+ // Load configuration from both sources
+ config, err := tmconfig.LoadFromSources(mainConfigFile.Name(), []string{mappingFile.Name()})
+ require.NoError(t, err)
+
+ // Verify that both mappers are loaded
+ require.Len(t, config.Lists, 2)
+
+ ids := make([]string, len(config.Lists))
+ for i, list := range config.Lists {
+ ids[i] = list.ID
+ }
+ assert.Contains(t, ids, "main-mapper")
+ assert.Contains(t, ids, "additional-mapper")
+
+ // Verify custom SDK and server are preserved from main config
+ assert.Equal(t, "https://custom.example.com/sdk.js", config.SDK)
+ assert.Equal(t, "https://custom.example.com/", config.Server)
+
+ // Create mapper and test it works
+ m, err := mapper.NewMapper(config.Lists)
+ require.NoError(t, err)
+ require.NotNil(t, m)
+}