blob: f72c5c5adfeef39ac4652c28455a77995d09fd59 [file] [log] [blame]
Akron32d53de2025-05-22 13:45:32 +02001package mapper
2
3import (
Akron32d53de2025-05-22 13:45:32 +02004 "fmt"
5
Akron2ef703c2025-07-03 15:57:42 +02006 "github.com/KorAP/Koral-Mapper/config"
7 "github.com/KorAP/Koral-Mapper/parser"
Akron32d53de2025-05-22 13:45:32 +02008)
9
10// Direction represents the mapping direction (A to B or B to A)
Akrona1a183f2025-05-26 17:47:33 +020011type Direction bool
Akron32d53de2025-05-22 13:45:32 +020012
13const (
Akrona1a183f2025-05-26 17:47:33 +020014 AtoB Direction = true
15 BtoA Direction = false
Akron2f93c582026-02-19 16:49:13 +010016
17 RewriteEditor = "Koral-Mapper"
Akron32d53de2025-05-22 13:45:32 +020018)
19
Akrona1a183f2025-05-26 17:47:33 +020020// String converts the Direction to its string representation
21func (d Direction) String() string {
22 if d {
23 return "atob"
24 }
25 return "btoa"
26}
27
28// ParseDirection converts a string direction to Direction type
29func ParseDirection(dir string) (Direction, error) {
30 switch dir {
31 case "atob":
32 return AtoB, nil
33 case "btoa":
34 return BtoA, nil
35 default:
36 return false, fmt.Errorf("invalid direction: %s", dir)
37 }
38}
39
Akron32d53de2025-05-22 13:45:32 +020040// Mapper handles the application of mapping rules to JSON objects
41type Mapper struct {
Akron2f93c582026-02-19 16:49:13 +010042 mappingLists map[string]*config.MappingList
43 parsedQueryRules map[string][]*parser.MappingResult
44 parsedCorpusRules map[string][]*parser.CorpusMappingResult
Akron32d53de2025-05-22 13:45:32 +020045}
46
Akrona00d4752025-05-26 17:34:36 +020047// NewMapper creates a new Mapper instance from a list of MappingLists
48func NewMapper(lists []config.MappingList) (*Mapper, error) {
Akron32d53de2025-05-22 13:45:32 +020049 m := &Mapper{
Akron2f93c582026-02-19 16:49:13 +010050 mappingLists: make(map[string]*config.MappingList),
51 parsedQueryRules: make(map[string][]*parser.MappingResult),
52 parsedCorpusRules: make(map[string][]*parser.CorpusMappingResult),
Akron32d53de2025-05-22 13:45:32 +020053 }
54
Akrona00d4752025-05-26 17:34:36 +020055 // Store mapping lists by ID
56 for _, list := range lists {
57 if _, exists := m.mappingLists[list.ID]; exists {
58 return nil, fmt.Errorf("duplicate mapping list ID found: %s", list.ID)
59 }
60
Akrona00d4752025-05-26 17:34:36 +020061 listCopy := list
62 m.mappingLists[list.ID] = &listCopy
63
Akron2f93c582026-02-19 16:49:13 +010064 if list.IsCorpus() {
65 corpusRules, err := list.ParseCorpusMappings()
66 if err != nil {
67 return nil, fmt.Errorf("failed to parse corpus mappings for list %s: %w", list.ID, err)
68 }
69 m.parsedCorpusRules[list.ID] = corpusRules
70 } else {
71 queryRules, err := list.ParseMappings()
72 if err != nil {
73 return nil, fmt.Errorf("failed to parse mappings for list %s: %w", list.ID, err)
74 }
75 m.parsedQueryRules[list.ID] = queryRules
Akron32d53de2025-05-22 13:45:32 +020076 }
Akron32d53de2025-05-22 13:45:32 +020077 }
78
79 return m, nil
80}
81
82// MappingOptions contains the options for applying mappings
83type MappingOptions struct {
Akron0d9117c2025-05-27 15:20:21 +020084 FoundryA string
85 LayerA string
86 FoundryB string
87 LayerB string
Akron41310262026-02-23 18:58:53 +010088 FieldA string
89 FieldB string
Akron0d9117c2025-05-27 15:20:21 +020090 Direction Direction
91 AddRewrites bool
Akron32d53de2025-05-22 13:45:32 +020092}
Akrone4f570d2026-02-20 08:18:06 +010093
Akron422cd252026-05-19 16:31:19 +020094// validateEffectiveOptions checks that the resolved source and target
95// identifiers are not identical, which would cause an infinite mapping loop.
96// For annotation mappings it compares the effective foundry+layer pair;
97// for corpus mappings it compares the effective field names.
98// The effective value is: query-parameter override if non-empty, otherwise
99// the YAML list default.
100func (m *Mapper) validateEffectiveOptions(mappingID string, opts MappingOptions) error {
101 list, exists := m.mappingLists[mappingID]
102 if !exists {
103 return nil // will be caught later
104 }
105
106 if list.IsCorpus() {
107 effFieldA := opts.FieldA
108 if effFieldA == "" {
109 effFieldA = list.FieldA
110 }
111 effFieldB := opts.FieldB
112 if effFieldB == "" {
113 effFieldB = list.FieldB
114 }
115 if effFieldA != "" && effFieldA == effFieldB {
116 return fmt.Errorf("identical source and target field (fieldA == fieldB == %q) in mapping list '%s': this would cause an infinite mapping loop", effFieldA, mappingID)
117 }
118 return nil
119 }
120
121 effFoundryA := opts.FoundryA
122 if effFoundryA == "" {
123 effFoundryA = list.FoundryA
124 }
125 effLayerA := opts.LayerA
126 if effLayerA == "" {
127 effLayerA = list.LayerA
128 }
129 effFoundryB := opts.FoundryB
130 if effFoundryB == "" {
131 effFoundryB = list.FoundryB
132 }
133 effLayerB := opts.LayerB
134 if effLayerB == "" {
135 effLayerB = list.LayerB
136 }
137
138 if effFoundryA != "" && effFoundryA == effFoundryB && effLayerA == effLayerB {
139 return fmt.Errorf("identical source and target (foundryA/layerA == foundryB/layerB == %q/%q) in mapping list '%s': this would cause an infinite mapping loop", effFoundryA, effLayerA, mappingID)
140 }
141
142 return nil
143}
144
Akrone4f570d2026-02-20 08:18:06 +0100145// CascadeQueryMappings applies multiple mapping lists sequentially,
146// feeding the output of each into the next. orderedIDs and
147// perMappingOpts must have the same length. An empty list returns
148// jsonData unchanged.
149func (m *Mapper) CascadeQueryMappings(orderedIDs []string, perMappingOpts []MappingOptions, jsonData any) (any, error) {
150 if len(orderedIDs) != len(perMappingOpts) {
151 return nil, fmt.Errorf("orderedIDs length (%d) must match perMappingOpts length (%d)", len(orderedIDs), len(perMappingOpts))
152 }
153
154 result := jsonData
155 for i, id := range orderedIDs {
156 var err error
157 result, err = m.ApplyQueryMappings(id, perMappingOpts[i], result)
158 if err != nil {
159 return nil, fmt.Errorf("cascade step %d (mapping %q): %w", i, id, err)
160 }
161 }
162 return result, nil
163}
164
165// CascadeResponseMappings applies multiple mapping lists sequentially
166// to a response object, feeding the output of each into the next.
167// orderedIDs and perMappingOpts must have the same length. An empty
168// list returns jsonData unchanged.
169func (m *Mapper) CascadeResponseMappings(orderedIDs []string, perMappingOpts []MappingOptions, jsonData any) (any, error) {
170 if len(orderedIDs) != len(perMappingOpts) {
171 return nil, fmt.Errorf("orderedIDs length (%d) must match perMappingOpts length (%d)", len(orderedIDs), len(perMappingOpts))
172 }
173
174 result := jsonData
175 for i, id := range orderedIDs {
176 var err error
177 result, err = m.ApplyResponseMappings(id, perMappingOpts[i], result)
178 if err != nil {
179 return nil, fmt.Errorf("cascade step %d (mapping %q): %w", i, id, err)
180 }
181 }
182 return result, nil
183}