Add configurable Kalamar integration
Change-Id: Ic07423dd7cc605509a364154bf4f37e4c13dc0d1
diff --git a/README.md b/README.md
index 1b0fc1a..a9835a4 100644
--- a/README.md
+++ b/README.md
@@ -25,19 +25,36 @@
## Configuration File Format
-Mapping rules are defined in YAML files with the following structure:
+Mapping rules are defined in a YAML configuration file.
```yaml
-- id: mapping-list-id
- foundryA: source-foundry
- layerA: source-layer
- foundryB: target-foundry
- layerB: target-layer
- mappings:
- - "[pattern1] <> [replacement1]"
- - "[pattern2] <> [replacement2]"
+# Optional: Custom SDK endpoint for Kalamar plugin integration
+sdk: "https://custom.example.com/js/korap-plugin.js"
+
+# Optional: Custom server endpoint for Kalamar plugin integration
+server: "https://custom.example.com/"
+
+# Mapping lists (same format as standard format)
+lists:
+ - id: mapping-list-id
+ foundryA: source-foundry
+ layerA: source-layer
+ foundryB: target-foundry
+ layerB: target-layer
+ mappings:
+ - "[pattern1] <> [replacement1]"
+ - "[pattern2] <> [replacement2]"
```
+The `sdk` and `server` fields are optional and override the default endpoints used for Kalamar plugin integration:
+
+- **`sdk`**: Custom SDK JavaScript file URL (default: `https://korap.ids-mannheim.de/js/korap-plugin-latest.js`)
+- **`server`**: Custom server endpoint URL (default: `https://korap.ids-mannheim.de/`)
+
+These values are applied during configuration parsing and affect the HTML plugin page served at the root endpoint (`/`).
+
+### Mapping Rules
+
Each mapping rule consists of two patterns separated by `<>`. The patterns can be:
- Simple terms: `[key]` or `[foundry/layer=key]` or `[foundry/layer=key:value]`
- Complex terms with AND/OR relations: `[term1 & term2]` or `[term1 | term2]` or `[term1 | (term2 & term3)]`
@@ -106,13 +123,27 @@
}
```
+### GET /
+
+Serves the Kalamar plugin integration page. This HTML page includes:
+
+- Plugin information and available mapping lists
+- JavaScript integration code for Kalamar
+- SDK and server endpoints configured via `sdk` and `server` configuration fields
+
+The SDK script and server data-attribute in the HTML are determined by the configuration file's `sdk` and `server` values, with fallback to default endpoints if not specified.
+
+### GET /health
+
+Health check endpoint that returns "OK" with HTTP 200 status.
+
## Progress
- [x] Mapping functionality
- [x] Support for rewrites
- [x] Web service
- [ ] Support for negation
-- [ ] JSON script for Kalamar integration
+- [x] JSON script for Kalamar integration
- [ ] Response rewriting
- [ ] Integration of mapping files
diff --git a/cmd/termmapper/fuzz_test.go b/cmd/termmapper/fuzz_test.go
index ee5c80d..650a08d 100644
--- a/cmd/termmapper/fuzz_test.go
+++ b/cmd/termmapper/fuzz_test.go
@@ -50,7 +50,7 @@
}
// Create mock config for testing
- mockConfig := &tmconfig.MappingLists{
+ mockConfig := &tmconfig.MappingConfig{
Lists: []tmconfig.MappingList{mappingList},
}
@@ -164,7 +164,7 @@
require.NoError(t, err)
// Create mock config for testing
- mockConfig := &tmconfig.MappingLists{
+ mockConfig := &tmconfig.MappingConfig{
Lists: []tmconfig.MappingList{mappingList},
}
diff --git a/cmd/termmapper/main.go b/cmd/termmapper/main.go
index 211dadc..60dc0a0 100644
--- a/cmd/termmapper/main.go
+++ b/cmd/termmapper/main.go
@@ -33,6 +33,8 @@
Hash string
Date string
Description string
+ Server string
+ SDK string
MappingIDs []string
}
@@ -114,7 +116,7 @@
}
}
-func setupRoutes(app *fiber.App, m *mapper.Mapper, yamlConfig *config.MappingLists) {
+func setupRoutes(app *fiber.App, m *mapper.Mapper, yamlConfig *config.MappingConfig) {
// Health check endpoint
app.Get("/health", func(c *fiber.Ctx) error {
return c.SendString("OK")
@@ -224,7 +226,7 @@
return nil
}
-func handleKalamarPlugin(yamlConfig *config.MappingLists) fiber.Handler {
+func handleKalamarPlugin(yamlConfig *config.MappingConfig) fiber.Handler {
return func(c *fiber.Ctx) error {
// Get list of available mapping IDs
var mappingIDs []string
@@ -232,6 +234,10 @@
mappingIDs = append(mappingIDs, list.ID)
}
+ // Use values from config (defaults are already applied during parsing)
+ server := yamlConfig.Server
+ sdk := yamlConfig.SDK
+
// Prepare template data
data := TemplateData{
Title: config.Title,
@@ -239,6 +245,8 @@
Hash: config.Buildhash,
Date: config.Buildtime,
Description: config.Description,
+ Server: server,
+ SDK: sdk,
MappingIDs: mappingIDs,
}
@@ -258,6 +266,8 @@
<head>
<meta charset="UTF-8">
<title>` + data.Title + `</title>
+ <script src="` + data.SDK + `"
+ data-server="` + data.Server + `"></script>
</head>
<body>
<div class="container">
@@ -291,6 +301,29 @@
html += `
</ul>
+
+ <script>
+ <!-- activates/deactivates Mapper. -->
+
+ let data = {
+ 'action' : 'pipe',
+ 'service' : 'https://korap.ids-mannheim.de/plugin/termmapper/query'
+ };
+
+ function pluginit (p) {
+ p.onMessage = function(msg) {
+ if (msg.key == 'termmapper') {
+ if (msg.value) {
+ data['job'] = 'add';
+ }
+ else {
+ data['job'] = 'del';
+ };
+ KorAPlugin.sendMsg(data);
+ };
+ };
+ };
+ </script>
</body>
</html>`
diff --git a/cmd/termmapper/main_test.go b/cmd/termmapper/main_test.go
index 5a9826b..292d3d9 100644
--- a/cmd/termmapper/main_test.go
+++ b/cmd/termmapper/main_test.go
@@ -34,7 +34,7 @@
require.NoError(t, err)
// Create mock config for testing
- mockConfig := &tmconfig.MappingLists{
+ mockConfig := &tmconfig.MappingConfig{
Lists: []tmconfig.MappingList{mappingList},
}
@@ -269,7 +269,7 @@
require.NoError(t, err)
// Create mock config for testing
- mockConfig := &tmconfig.MappingLists{
+ mockConfig := &tmconfig.MappingConfig{
Lists: []tmconfig.MappingList{mappingList},
}
@@ -299,3 +299,89 @@
assert.Contains(t, string(body), "KoralPipe-TermMapper")
}
+
+func TestKalamarPluginWithCustomSdkAndServer(t *testing.T) {
+ // Create test mapping list
+ mappingList := tmconfig.MappingList{
+ ID: "test-mapper",
+ Mappings: []tmconfig.MappingRule{
+ "[A] <> [B]",
+ },
+ }
+
+ // Create mapper
+ m, err := mapper.NewMapper([]tmconfig.MappingList{mappingList})
+ require.NoError(t, err)
+
+ tests := []struct {
+ name string
+ customSDK string
+ customServer string
+ expectedSDK string
+ expectedServer string
+ }{
+ {
+ name: "Custom SDK and Server values",
+ customSDK: "https://custom.example.com/custom-sdk.js",
+ customServer: "https://custom.example.com/",
+ expectedSDK: "https://custom.example.com/custom-sdk.js",
+ expectedServer: "https://custom.example.com/",
+ },
+ {
+ name: "Only custom SDK value",
+ customSDK: "https://custom.example.com/custom-sdk.js",
+ customServer: "https://korap.ids-mannheim.de/", // defaults applied during parsing
+ expectedSDK: "https://custom.example.com/custom-sdk.js",
+ expectedServer: "https://korap.ids-mannheim.de/",
+ },
+ {
+ name: "Only custom Server value",
+ customSDK: "https://korap.ids-mannheim.de/js/korap-plugin-latest.js", // defaults applied during parsing
+ customServer: "https://custom.example.com/",
+ expectedSDK: "https://korap.ids-mannheim.de/js/korap-plugin-latest.js",
+ expectedServer: "https://custom.example.com/",
+ },
+ {
+ name: "Defaults applied during parsing",
+ customSDK: "https://korap.ids-mannheim.de/js/korap-plugin-latest.js", // defaults applied during parsing
+ customServer: "https://korap.ids-mannheim.de/", // defaults applied during parsing
+ expectedSDK: "https://korap.ids-mannheim.de/js/korap-plugin-latest.js",
+ expectedServer: "https://korap.ids-mannheim.de/",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Create mock config with custom values
+ mockConfig := &tmconfig.MappingConfig{
+ SDK: tt.customSDK,
+ Server: tt.customServer,
+ Lists: []tmconfig.MappingList{mappingList},
+ }
+
+ // Create fiber app
+ app := fiber.New()
+ setupRoutes(app, m, mockConfig)
+
+ // Test Kalamar plugin endpoint
+ req := httptest.NewRequest(http.MethodGet, "/", nil)
+ resp, err := app.Test(req)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
+ body, err := io.ReadAll(resp.Body)
+ require.NoError(t, err)
+
+ htmlContent := string(body)
+
+ // Check that the HTML contains the expected SDK and Server values
+ assert.Contains(t, htmlContent, `src="`+tt.expectedSDK+`"`)
+ assert.Contains(t, htmlContent, `data-server="`+tt.expectedServer+`"`)
+
+ // Ensure it's still a valid HTML page
+ assert.Contains(t, htmlContent, "KoralPipe-TermMapper")
+ assert.Contains(t, htmlContent, "<!DOCTYPE html>")
+ })
+ }
+}
diff --git a/config/config.go b/config/config.go
index 6bafaad..128c063 100644
--- a/config/config.go
+++ b/config/config.go
@@ -9,6 +9,11 @@
"gopkg.in/yaml.v3"
)
+const (
+ defaultServer = "https://korap.ids-mannheim.de/"
+ defaultSDK = "https://korap.ids-mannheim.de/js/korap-plugin-latest.js"
+)
+
// MappingRule represents a single mapping rule in the configuration
type MappingRule string
@@ -22,13 +27,15 @@
Mappings []MappingRule `yaml:"mappings"`
}
-// MappingLists represents the root configuration containing multiple mapping lists
-type MappingLists struct {
- Lists []MappingList
+// MappingConfig represents the root configuration containing multiple mapping lists
+type MappingConfig struct {
+ SDK string `yaml:"sdk,omitempty"`
+ Server string `yaml:"server,omitempty"`
+ Lists []MappingList `yaml:"lists,omitempty"`
}
// LoadConfig loads a YAML configuration file and returns a Config object
-func LoadConfig(filename string) (*MappingLists, error) {
+func LoadConfig(filename string) (*MappingConfig, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
@@ -39,37 +46,71 @@
return nil, fmt.Errorf("EOF: config file is empty")
}
+ // Try to unmarshal as new format first (object with optional sdk/server and lists)
+ var config MappingConfig
+ if err := yaml.Unmarshal(data, &config); err == nil && len(config.Lists) > 0 {
+ // Successfully parsed as new format with lists field
+ if err := validateMappingLists(config.Lists); err != nil {
+ return nil, err
+ }
+ // Apply defaults if not specified
+ applyDefaults(&config)
+ return &config, nil
+ }
+
+ // Fall back to old format (direct list)
var lists []MappingList
if err := yaml.Unmarshal(data, &lists); err != nil {
return nil, fmt.Errorf("failed to parse YAML: %w", err)
}
+ if err := validateMappingLists(lists); err != nil {
+ return nil, err
+ }
+
+ config = MappingConfig{Lists: lists}
+ // Apply defaults if not specified
+ applyDefaults(&config)
+ return &config, nil
+}
+
+// applyDefaults sets default values for SDK and Server if they are empty
+func applyDefaults(config *MappingConfig) {
+ if config.SDK == "" {
+ config.SDK = defaultSDK
+ }
+ if config.Server == "" {
+ config.Server = defaultServer
+ }
+}
+
+// validateMappingLists validates a slice of mapping lists
+func validateMappingLists(lists []MappingList) error {
// Validate the configuration
seenIDs := make(map[string]bool)
for i, list := range lists {
if list.ID == "" {
- return nil, fmt.Errorf("mapping list at index %d is missing an ID", i)
+ return fmt.Errorf("mapping list at index %d is missing an ID", i)
}
// Check for duplicate IDs
if seenIDs[list.ID] {
- return nil, fmt.Errorf("duplicate mapping list ID found: %s", list.ID)
+ return fmt.Errorf("duplicate mapping list ID found: %s", list.ID)
}
seenIDs[list.ID] = true
if len(list.Mappings) == 0 {
- return nil, fmt.Errorf("mapping list '%s' has no mapping rules", list.ID)
+ return fmt.Errorf("mapping list '%s' has no mapping rules", list.ID)
}
// Validate each mapping rule
for j, rule := range list.Mappings {
if rule == "" {
- return nil, fmt.Errorf("mapping list '%s' rule at index %d is empty", list.ID, j)
+ return fmt.Errorf("mapping list '%s' rule at index %d is empty", list.ID, j)
}
}
}
-
- return &MappingLists{Lists: lists}, nil
+ return nil
}
// ParseMappings parses all mapping rules in a list and returns a slice of parsed rules
diff --git a/config/config_test.go b/config/config_test.go
index 9dc64ca..6519f82 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -574,3 +574,120 @@
assert.Equal(t, expectedValue, pronTypeTerm.Key)
}
}
+
+func TestConfigWithSdkAndServer(t *testing.T) {
+ tests := []struct {
+ name string
+ content string
+ expectedSDK string
+ expectedServer string
+ wantErr bool
+ }{
+ {
+ name: "Configuration with SDK and Server values",
+ content: `
+sdk: "https://custom.example.com/sdk.js"
+server: "https://custom.example.com/"
+lists:
+- id: test-mapper
+ foundryA: opennlp
+ layerA: p
+ foundryB: upos
+ layerB: p
+ mappings:
+ - "[A] <> [B]"
+`,
+ expectedSDK: "https://custom.example.com/sdk.js",
+ expectedServer: "https://custom.example.com/",
+ wantErr: false,
+ },
+ {
+ name: "Configuration with only SDK value",
+ content: `
+sdk: "https://custom.example.com/sdk.js"
+lists:
+- id: test-mapper
+ mappings:
+ - "[A] <> [B]"
+`,
+ expectedSDK: "https://custom.example.com/sdk.js",
+ expectedServer: "https://korap.ids-mannheim.de/", // default applied
+ wantErr: false,
+ },
+ {
+ name: "Configuration with only Server value",
+ content: `
+server: "https://custom.example.com/"
+lists:
+- id: test-mapper
+ mappings:
+ - "[A] <> [B]"
+`,
+ expectedSDK: "https://korap.ids-mannheim.de/js/korap-plugin-latest.js", // default applied
+ expectedServer: "https://custom.example.com/",
+ wantErr: false,
+ },
+ {
+ name: "Configuration without SDK and Server (old format with defaults applied)",
+ content: `
+- id: test-mapper
+ mappings:
+ - "[A] <> [B]"
+`,
+ expectedSDK: "https://korap.ids-mannheim.de/js/korap-plugin-latest.js", // default applied
+ expectedServer: "https://korap.ids-mannheim.de/", // default applied
+ wantErr: false,
+ },
+ {
+ name: "Configuration with lists field explicitly",
+ content: `
+sdk: "https://custom.example.com/sdk.js"
+server: "https://custom.example.com/"
+lists:
+- id: test-mapper-1
+ mappings:
+ - "[A] <> [B]"
+- id: test-mapper-2
+ mappings:
+ - "[C] <> [D]"
+`,
+ expectedSDK: "https://custom.example.com/sdk.js",
+ expectedServer: "https://custom.example.com/",
+ wantErr: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tmpfile, err := os.CreateTemp("", "config-*.yaml")
+ require.NoError(t, err)
+ defer os.Remove(tmpfile.Name())
+
+ _, err = tmpfile.WriteString(tt.content)
+ require.NoError(t, err)
+ err = tmpfile.Close()
+ require.NoError(t, err)
+
+ config, err := LoadConfig(tmpfile.Name())
+ if tt.wantErr {
+ require.Error(t, err)
+ return
+ }
+
+ require.NoError(t, err)
+ require.NotNil(t, config)
+
+ // Check SDK and Server values
+ assert.Equal(t, tt.expectedSDK, config.SDK)
+ assert.Equal(t, tt.expectedServer, config.Server)
+
+ // Ensure lists are still loaded correctly
+ require.Greater(t, len(config.Lists), 0)
+
+ // Verify first mapping list
+ firstList := config.Lists[0]
+ assert.NotEmpty(t, firstList.ID)
+ assert.Greater(t, len(firstList.Mappings), 0)
+ })
+ }
+}